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,893 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getDouyinResolution = getDouyinResolution;
7
+ exports.generateDouyinASS = generateDouyinASS;
8
+ exports.burnDouyinDanmaku = burnDouyinDanmaku;
9
+ /**
10
+ * 抖音弹幕处理模块
11
+ * 负责抖音弹幕数据转换、ASS 字幕生成、视频烧录等功能
12
+ * 抖音弹幕只有滚动弹幕:纯文字弹幕走 ASS \move 烧录;
13
+ * 含平台表情或被选中展示点赞角标的弹幕预渲染为透明 PNG 条,通过 overlay 滤镜叠加(libass 不支持内嵌图片)
14
+ */
15
+ const node_crypto_1 = __importDefault(require("node:crypto"));
16
+ const node_fs_1 = __importDefault(require("node:fs"));
17
+ const node_os_1 = __importDefault(require("node:os"));
18
+ const node_path_1 = __importDefault(require("node:path"));
19
+ const node_karin_1 = require("../../../compat/node-karin.js");
20
+ const utils_1 = require("../../module/utils");
21
+ const amagiClient_1 = require("../../module/utils/amagiClient");
22
+ // ==================== 编码器检测与参数 ====================
23
+ const ENCODER_PRIORITY = {
24
+ h264: ['h264_nvenc', 'h264_qsv', 'h264_amf', 'libx264'],
25
+ h265: ['hevc_nvenc', 'hevc_qsv', 'hevc_amf', 'libx265'],
26
+ av1: ['av1_nvenc', 'av1_qsv', 'av1_amf', 'libsvtav1', 'libaom-av1']
27
+ };
28
+ const SOFTWARE_FALLBACK = {
29
+ h264: 'libx264',
30
+ h265: 'libx265',
31
+ av1: 'libsvtav1'
32
+ };
33
+ const cachedEncoders = {};
34
+ /**
35
+ * 按优先级探测可用的硬件/软件编码器(结果缓存)
36
+ * @param codec 目标编码格式
37
+ * @returns 可用的 ffmpeg 编码器名
38
+ */
39
+ async function detectEncoder(codec) {
40
+ if (cachedEncoders[codec])
41
+ return cachedEncoders[codec];
42
+ node_karin_1.logger.debug(`[DouyinDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
43
+ for (const encoder of ENCODER_PRIORITY[codec]) {
44
+ try {
45
+ const result = await (0, node_karin_1.ffmpeg)(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`);
46
+ if (result.status) {
47
+ cachedEncoders[codec] = encoder;
48
+ node_karin_1.logger.info(`[DouyinDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
49
+ return encoder;
50
+ }
51
+ }
52
+ catch {
53
+ /* ignore */
54
+ }
55
+ }
56
+ const fallback = SOFTWARE_FALLBACK[codec];
57
+ cachedEncoders[codec] = fallback;
58
+ node_karin_1.logger.info(`[DouyinDanmaku] 回退到软件编码器: ${fallback}`);
59
+ return fallback;
60
+ }
61
+ /**
62
+ * 获取视频平均码率
63
+ * @param path 视频文件路径
64
+ * @returns 平均码率(kbps),获取失败返回 0
65
+ */
66
+ async function getVideoBitrate(path) {
67
+ // 用文件大小和时长计算平均码率,这是最准确的方式
68
+ try {
69
+ const fileSize = node_fs_1.default.statSync(path).size;
70
+ const { stdout } = await (0, node_karin_1.ffprobe)(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path}"`);
71
+ const duration = parseFloat(stdout.trim());
72
+ if (duration > 0 && fileSize > 0) {
73
+ return Math.round((fileSize * 8) / duration / 1000);
74
+ }
75
+ }
76
+ catch {
77
+ /* ignore */
78
+ }
79
+ // 尝试从流信息获取
80
+ try {
81
+ const { stdout } = await (0, node_karin_1.ffprobe)(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path}"`);
82
+ const bitrate = parseInt(stdout.trim());
83
+ if (bitrate > 0)
84
+ return Math.round(bitrate / 1000);
85
+ }
86
+ catch {
87
+ /* ignore */
88
+ }
89
+ return 0;
90
+ }
91
+ /** 各编码器的目标码率模式参数模板 */
92
+ const BITRATE_MODE_PARAMS = {
93
+ h264_nvenc: ({ b, m, u }) => `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${b} -maxrate ${m} -bufsize ${u}`,
94
+ h264_qsv: ({ b, m, u }) => `-c:v h264_qsv -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u}`,
95
+ h264_amf: ({ b, m }) => `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${b} -maxrate ${m}`,
96
+ libx264: ({ b, m, u, t }) => `-c:v libx264 -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u} -threads ${t}`,
97
+ hevc_nvenc: ({ b, m, u }) => `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${b} -maxrate ${m} -bufsize ${u}`,
98
+ hevc_qsv: ({ b, m, u }) => `-c:v hevc_qsv -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u}`,
99
+ hevc_amf: ({ b, m }) => `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${b} -maxrate ${m}`,
100
+ libx265: ({ b, m, u, t }) => `-c:v libx265 -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u} -threads ${t}`,
101
+ av1_nvenc: ({ b, m, u }) => `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${b} -maxrate ${m} -bufsize ${u}`,
102
+ av1_qsv: ({ b, m, u }) => `-c:v av1_qsv -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u}`,
103
+ av1_amf: ({ b, m }) => `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${b} -maxrate ${m}`,
104
+ libsvtav1: ({ b, m, u, t }) => `-c:v libsvtav1 -preset 6 -b:v ${b} -maxrate ${m} -bufsize ${u} -threads ${t}`,
105
+ 'libaom-av1': ({ b, m, u, t }) => `-c:v libaom-av1 -cpu-used 4 -b:v ${b} -maxrate ${m} -bufsize ${u} -threads ${t}`
106
+ };
107
+ /** 各编码器的 CRF/CQ 兜底模式参数模板(无码率信息时使用) */
108
+ const CRF_MODE_PARAMS = {
109
+ h264_nvenc: () => '-c:v h264_nvenc -preset p4 -rc vbr -cq 23',
110
+ h264_qsv: () => '-c:v h264_qsv -preset medium -global_quality 23',
111
+ h264_amf: () => '-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23',
112
+ libx264: (t) => `-c:v libx264 -crf 23 -preset medium -threads ${t}`,
113
+ hevc_nvenc: () => '-c:v hevc_nvenc -preset p4 -rc vbr -cq 28',
114
+ hevc_qsv: () => '-c:v hevc_qsv -preset medium -global_quality 28',
115
+ hevc_amf: () => '-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28',
116
+ libx265: (t) => `-c:v libx265 -crf 28 -preset medium -threads ${t}`,
117
+ av1_nvenc: () => '-c:v av1_nvenc -preset p4 -rc vbr -cq 30',
118
+ av1_qsv: () => '-c:v av1_qsv -preset medium -global_quality 30',
119
+ av1_amf: () => '-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30',
120
+ libsvtav1: (t) => `-c:v libsvtav1 -crf 30 -preset 6 -threads ${t}`,
121
+ 'libaom-av1': (t) => `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${t}`
122
+ };
123
+ /**
124
+ * 获取编码器参数
125
+ * @param encoder detectEncoder 探测到的编码器名
126
+ * @param targetBitrate 源视频平均码率(kbps),为空或 0 时使用 CRF/CQ 兜底模式
127
+ * @returns ffmpeg 视频编码参数串
128
+ */
129
+ function getEncoderParams(encoder, targetBitrate) {
130
+ const threads = Math.max(1, Math.floor(node_os_1.default.cpus().length / 2));
131
+ // 有码率时用目标码率模式
132
+ // 二次编码会损失画质,目标码率设为原视频的 1.4 倍补偿,确保输出更清晰
133
+ if (targetBitrate && targetBitrate > 0) {
134
+ const adjusted = Math.round(targetBitrate * 1.4);
135
+ const params = {
136
+ b: `${adjusted}k`,
137
+ m: `${Math.round(adjusted * 2.5)}k`,
138
+ u: `${Math.round(adjusted * 4)}k`,
139
+ t: threads
140
+ };
141
+ const build = BITRATE_MODE_PARAMS[encoder] ?? BITRATE_MODE_PARAMS.libx265;
142
+ return build(params);
143
+ }
144
+ const build = CRF_MODE_PARAMS[encoder] ?? CRF_MODE_PARAMS.libx265;
145
+ return build(threads);
146
+ }
147
+ // ==================== 内部工具函数 ====================
148
+ /** 输出帧率:弹幕逐帧重算位置,高帧率下步进更小、观感更顺滑(视频帧复制,内容不变) */
149
+ const OUTPUT_FPS = 60;
150
+ /**
151
+ * 毫秒转 ASS 时间戳(h:mm:ss.cc,厘秒精度)
152
+ * @param ms 毫秒时间
153
+ * @returns ASS 时间戳字符串
154
+ */
155
+ const toASSTime = (ms) => {
156
+ const s = ms / 1000;
157
+ const h = Math.floor(s / 3600);
158
+ const m = Math.floor((s % 3600) / 60);
159
+ const sec = Math.floor(s % 60);
160
+ const cs = Math.floor((s % 1) * 100);
161
+ return `${h}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}.${cs.toString().padStart(2, '0')}`;
162
+ };
163
+ /**
164
+ * 估算文本宽度(全角字符按 1 倍字号、半角按 0.5 倍)
165
+ * @param text 文本内容
166
+ * @param fontSize 字号(像素)
167
+ * @returns 估算宽度(像素)
168
+ */
169
+ const estimateWidth = (text, fontSize) => {
170
+ let w = 0;
171
+ for (const c of text) {
172
+ w += c.charCodeAt(0) > 127 ? fontSize : fontSize * 0.5;
173
+ }
174
+ return w;
175
+ };
176
+ /**
177
+ * ASS 文本转义
178
+ * @param text 原始文本
179
+ * @returns 转义后的文本
180
+ */
181
+ const escapeASS = (text) => text.replace(/\\/g, '\\\\').replace(/\{/g, '\\{').replace(/\}/g, '\\}').replace(/\n/g, '\\N');
182
+ /**
183
+ * HTML 文本转义
184
+ * @param text 原始文本
185
+ * @returns 转义后的文本
186
+ */
187
+ const escapeHTML = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
188
+ /**
189
+ * Windows 路径转义为正斜杠形式(供 ffmpeg 滤镜内使用,盘符冒号需转义)
190
+ * @param path 原始路径
191
+ * @returns 转义后的路径
192
+ */
193
+ const escapeWinPath = (path) => path.replace(/\\/g, '/').replace(/:/g, '\\:');
194
+ /**
195
+ * 判断是否为横屏
196
+ * @param w 宽度
197
+ * @param h 高度
198
+ * @returns 宽大于高时为 true
199
+ */
200
+ const isLandscape = (w, h) => w > h;
201
+ /**
202
+ * 从 PNG 文件头读取尺寸
203
+ * @param buffer PNG 文件内容
204
+ * @returns 宽高(像素),非 PNG 返回空对象
205
+ */
206
+ const getPngSize = (buffer) => {
207
+ if (buffer.length >= 24 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
208
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
209
+ }
210
+ return {};
211
+ };
212
+ // ==================== FFprobe 工具 ====================
213
+ /**
214
+ * 获取视频分辨率
215
+ * @param path 视频文件路径
216
+ * @returns 宽高,获取失败返回抖音默认竖屏 1080x1920
217
+ */
218
+ async function getDouyinResolution(path) {
219
+ try {
220
+ const { stdout } = await (0, node_karin_1.ffprobe)(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path}"`);
221
+ const [w, h] = stdout.trim().split('x').map(Number);
222
+ if (w && h)
223
+ return { width: w, height: h };
224
+ }
225
+ catch {
226
+ /* ignore */
227
+ }
228
+ try {
229
+ const result = await (0, node_karin_1.ffmpeg)(`-i "${path}" -f null -`, { timeout: 5000 });
230
+ const stderr = result.stderr || '';
231
+ const match = stderr.match(/(\d{3,4})x(\d{3,4})/);
232
+ if (match)
233
+ return { width: parseInt(match[1]), height: parseInt(match[2]) };
234
+ }
235
+ catch {
236
+ /* ignore */
237
+ }
238
+ return { width: 1080, height: 1920 }; // 抖音默认竖屏
239
+ }
240
+ // ==================== 平台表情与弹幕条渲染 ====================
241
+ /**
242
+ * 获取抖音平台表情列表
243
+ * 与评论处理一致:name 为文本占位符(如 [捂脸]),url 为表情图地址
244
+ * 获取失败时返回空列表,含表情的弹幕将退化为纯文字烧录
245
+ * @returns 表情列表(按名称长度降序,避免短名抢先匹配)
246
+ */
247
+ async function fetchDouyinEmojiList() {
248
+ try {
249
+ const res = await amagiClient_1.douyinFetcher.fetchEmojiList();
250
+ const list = res.data?.emoji_list ?? [];
251
+ return list
252
+ .map((item) => ({ name: item.display_name, url: item.emoji_url?.url_list?.[0] ?? '' }))
253
+ .filter((item) => Boolean(item.name) && Boolean(item.url))
254
+ .sort((a, b) => b.name.length - a.name.length);
255
+ }
256
+ catch (err) {
257
+ node_karin_1.logger.warn('[DouyinDanmaku] 获取表情列表失败,表情弹幕将按纯文字处理', err);
258
+ return [];
259
+ }
260
+ }
261
+ /**
262
+ * 判断弹幕文本是否包含平台表情
263
+ * @param text 弹幕文本
264
+ * @param emojiList 表情列表
265
+ * @returns 包含任一表情占位符时为 true
266
+ */
267
+ const hasEmoji = (text, emojiList) => emojiList.some((e) => text.includes(e.name));
268
+ /**
269
+ * 把弹幕文本拆成文字/表情片段(长名优先匹配,与评论处理一致)
270
+ * @param text 弹幕文本
271
+ * @param emojiList 表情列表(已按名称长度降序)
272
+ * @returns 有序的片段列表
273
+ */
274
+ function splitDanmakuSegments(text, emojiList) {
275
+ const segments = [];
276
+ let buffer = '';
277
+ let index = 0;
278
+ const pushBuffer = () => {
279
+ if (buffer.length > 0) {
280
+ segments.push({ type: 'text', content: buffer });
281
+ buffer = '';
282
+ }
283
+ };
284
+ while (index < text.length) {
285
+ const matched = emojiList.find((e) => text.startsWith(e.name, index));
286
+ if (matched) {
287
+ pushBuffer();
288
+ segments.push({ type: 'emoji', name: matched.name, url: matched.url });
289
+ index += matched.name.length;
290
+ continue;
291
+ }
292
+ buffer += text[index];
293
+ index += 1;
294
+ }
295
+ pushBuffer();
296
+ return segments;
297
+ }
298
+ /** 抖音官方点赞图标(同 packages/template/src/components/platforms/douyin/Icons.tsx 的 DouyinLikeIcon) */
299
+ const LIKE_ICON_PATH = 'M453.036 88.712C493.774 30.664 560.66 0 634.251 0 774.403 0 890.972 121.59 890.972 266.137v.054c.01-.02.019-.04.028-.06 0 4.121-.07 7.241-.122 9.537-.072 3.151-.108 4.747.122 5.247-.531 30.403-5.778 55.522-15.101 88.712-5.289 5.96-10.204 17.184-15.1 29.572-7.724 11.998-10.647 17.81-15.101 29.57a546.67 546.67 0 0 1-14.452 22.974c-37.449 56.8-87.537 113.33-137.579 163.509-78.331 79.025-158.123 144.805-192.835 173.422-9.585 7.901-15.732 12.969-17.464 14.7-12.301 12.303-24.603 12.611-36.905 12.619-.324.004-.649.006-.977.006-25.236 0-37.854-12.619-50.472-25.237-.963-.963-3.413-2.984-7.156-5.979-38.233-28.184-124.273-96.997-205.116-180C121.066 542.02 61.622 470.007 29.092 399.588 16.474 374.351.731 314.264 0 280.922c.269-.267.227-1.873.144-5.078C.083 273.498 0 270.297 0 266.137 0 121.524 116.502 0 256.721 0c73.458 0 140.405 30.664 196.315 88.712Z';
300
+ /**
301
+ * 生成弹幕条 HTML:文字 + 内联表情图 + 可选点赞角标(白色图标 + 点赞数),透明背景,样式对齐 ASS(白字黑描边)
302
+ * @param segments 弹幕内容片段
303
+ * @param fontSize CSS 字号(像素)
304
+ * @param fontName 字体名
305
+ * @param opacity 整体不透明度(0-1)
306
+ * @param likeLabel 点赞角标文本(如 "14"、"2.4w"),为 null 时不渲染角标
307
+ * @returns 完整 HTML 文档
308
+ */
309
+ function buildStripHTML(segments, fontSize, fontName, opacity, likeLabel) {
310
+ const safeFontName = fontName.replace(/['\\]/g, '');
311
+ const strokeW = Math.max(1, Math.round(fontSize / 21));
312
+ const imgSize = Math.round(fontSize * 1.1);
313
+ const body = segments
314
+ .map((seg) => seg.type === 'text'
315
+ ? `<span>${escapeHTML(seg.content)}</span>`
316
+ : `<img src="${escapeHTML(seg.url)}" alt="${escapeHTML(seg.name)}" referrerpolicy="no-referrer" crossorigin="anonymous" />`)
317
+ .join('') +
318
+ (likeLabel !== null
319
+ ? `<svg viewBox="0 0 891 816" class="like-icon" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="${LIKE_ICON_PATH}" /></svg><span class="like-count">${escapeHTML(likeLabel)}</span>`
320
+ : '');
321
+ return `<!DOCTYPE html>
322
+ <html><head><meta charset="utf-8" /><style>
323
+ html,body{margin:0;padding:0;background:transparent}
324
+ #container{display:inline-flex;align-items:center;white-space:nowrap;font-family:'${safeFontName}',sans-serif;font-size:${fontSize}px;line-height:1.2;color:#fff;opacity:${opacity}}
325
+ #container span{paint-order:stroke fill;-webkit-text-stroke:${strokeW}px rgba(0,0,0,0.85)}
326
+ #container img{height:${imgSize}px;width:auto;display:inline-block}
327
+ .like-icon{height:0.95em;width:0.95em;display:inline-block;margin-left:0.35em}
328
+ .like-count{margin-left:0.25em}
329
+ </style></head><body><div id="container">${body}</div></body></html>`;
330
+ }
331
+ /**
332
+ * 把含表情/点赞的弹幕渲染成透明 PNG 条(按内容缓存,相同文本复用同一张图)
333
+ * 渲染失败时返回 null,调用方退化为纯文字 ASS 烧录
334
+ * @param text 弹幕原文
335
+ * @param likeLabel 点赞角标文本,为 null 时不渲染角标
336
+ * @param emojiList 表情列表
337
+ * @param style 渲染样式
338
+ * @param cache 渲染缓存(内容 hash -> 渲染 Promise)
339
+ * @param tempFiles 临时文件收集器(HTML/PNG 路径会追加进去,由调用方统一清理)
340
+ * @returns 弹幕条信息,失败返回 null
341
+ */
342
+ async function renderDanmakuStrip(text, likeLabel, emojiList, style, cache, tempFiles) {
343
+ const cacheKey = `${style.fontSize}:${style.fontName}:${style.opacity}:${likeLabel ?? ''}:${text}`;
344
+ let pending = cache.get(cacheKey);
345
+ if (!pending) {
346
+ pending = (async () => {
347
+ const hash = node_crypto_1.default.createHash('md5').update(cacheKey).digest('hex').slice(0, 12);
348
+ const htmlPath = `${utils_1.Common.tempDri.video}danmaku_strip_${hash}.html`;
349
+ const pngPath = `${utils_1.Common.tempDri.video}danmaku_strip_${hash}.png`;
350
+ tempFiles.push(htmlPath, pngPath);
351
+ try {
352
+ const segments = splitDanmakuSegments(text, emojiList);
353
+ node_fs_1.default.writeFileSync(htmlPath, buildStripHTML(segments, style.fontSize, style.fontName, style.opacity, likeLabel), 'utf-8');
354
+ const renderResult = (await node_karin_1.render.render({
355
+ name: 'koishi-plugin-kkk/danmaku',
356
+ file: htmlPath,
357
+ selector: '#container',
358
+ fullPage: false,
359
+ type: 'png',
360
+ omitBackground: true,
361
+ pageGotoParams: { waitUntil: 'load', timeout: 15000 }
362
+ }));
363
+ const base64 = Array.isArray(renderResult) ? renderResult[0] : renderResult;
364
+ const pngBuffer = Buffer.from(base64, 'base64');
365
+ const size = getPngSize(pngBuffer);
366
+ if (!size.width || !size.height)
367
+ throw new Error('无法读取弹幕条 PNG 尺寸');
368
+ node_fs_1.default.writeFileSync(pngPath, pngBuffer);
369
+ return { text, pngPath, width: size.width, height: size.height };
370
+ }
371
+ catch (err) {
372
+ node_karin_1.logger.warn(`[DouyinDanmaku] 弹幕条渲染失败,将按纯文字处理: ${text}`, err);
373
+ return null;
374
+ }
375
+ })();
376
+ cache.set(cacheKey, pending);
377
+ }
378
+ return pending;
379
+ }
380
+ /**
381
+ * 预渲染需要 PNG 条的弹幕(含平台表情,或被选中展示点赞角标)
382
+ * 渲染失败的弹幕退化为纯文字 ASS 烧录
383
+ * @param sortedDanmaku 按出现时间升序的弹幕列表
384
+ * @param emojiList 表情列表
385
+ * @param liked 点赞角标选择结果
386
+ * @param style 渲染样式
387
+ * @returns 弹幕 ID -> PNG 条 的映射,以及过程中产生的临时文件
388
+ */
389
+ async function prepareDanmakuStrips(sortedDanmaku, emojiList, liked, style) {
390
+ const strips = new Map();
391
+ const tempFiles = [];
392
+ const needsStrip = (dm) => liked.ids.has(dm.danmaku_id) || (emojiList.length > 0 && hasEmoji(dm.text, emojiList));
393
+ if (!sortedDanmaku.some(needsStrip))
394
+ return { strips, tempFiles };
395
+ const cache = new Map();
396
+ for (const dm of sortedDanmaku) {
397
+ if (!needsStrip(dm))
398
+ continue;
399
+ const likeLabel = liked.ids.has(dm.danmaku_id) ? formatLikeCount(dm.digg_count ?? 0) : null;
400
+ const strip = await renderDanmakuStrip(dm.text, likeLabel, emojiList, style, cache, tempFiles);
401
+ if (strip)
402
+ strips.set(dm.danmaku_id, strip);
403
+ }
404
+ return { strips, tempFiles };
405
+ }
406
+ // ==================== 点赞角标 ====================
407
+ /**
408
+ * 格式化点赞数:破万保留一位小数并加 w 后缀,只舍不入(四舍不能五入)
409
+ * 如 14 -> "14",24800 -> "2.4w",1001900 -> "100.1w"
410
+ * @param count 点赞数
411
+ * @returns 格式化后的文本
412
+ */
413
+ const formatLikeCount = (count) => {
414
+ if (count < 10000)
415
+ return String(count);
416
+ return `${(Math.floor(count / 1000) / 10).toFixed(1)}w`;
417
+ };
418
+ /**
419
+ * 从有点赞的弹幕中选出展示点赞角标的子集
420
+ * 全部展示会非常拥挤,目标展示数随弹幕总量按 1.5√N 缩放(100 条弹幕约展示 15 条),
421
+ * 弹幕越多比例越低(不是固定比例);再按点赞量从高到低截取
422
+ * @param sortedDanmaku 按出现时间升序的弹幕列表
423
+ * @returns 选中的弹幕 ID 集合及统计
424
+ */
425
+ function selectLikedDanmaku(sortedDanmaku) {
426
+ const candidates = sortedDanmaku.filter((dm) => (dm.digg_count ?? 0) > 0);
427
+ const target = Math.min(50, Math.max(5, Math.round(Math.sqrt(sortedDanmaku.length) * 1.5)));
428
+ if (candidates.length === 0)
429
+ return { ids: new Set(), candidateCount: 0, target };
430
+ if (candidates.length <= target)
431
+ return { ids: new Set(candidates.map((dm) => dm.danmaku_id)), candidateCount: candidates.length, target };
432
+ // 按点赞量降序取前 target 条;与第 target 条点赞数并列的一并保留,避免随意截断
433
+ const byDiggDesc = [...candidates].sort((a, b) => (b.digg_count ?? 0) - (a.digg_count ?? 0));
434
+ const cutoff = byDiggDesc[target - 1].digg_count ?? 0;
435
+ return {
436
+ ids: new Set(byDiggDesc.filter((dm) => (dm.digg_count ?? 0) >= cutoff).map((dm) => dm.danmaku_id)),
437
+ candidateCount: candidates.length,
438
+ target
439
+ };
440
+ }
441
+ // ==================== 弹幕布局 ====================
442
+ /** 字号配置映射(1080 高度基准) */
443
+ const FONT_SIZE_MAP = {
444
+ small: { base: 25, trackH: 30 },
445
+ medium: { base: 32, trackH: 38 },
446
+ large: { base: 40, trackH: 46 }
447
+ };
448
+ /**
449
+ * 计算弹幕布局参数
450
+ * @param height 画布高度(像素),所有几何以 1080 高为基准等比缩放
451
+ * @param danmakuArea 弹幕显示区域比例(0.25/0.5/0.75/1)
452
+ * @param danmakuFontSize 弹幕字号档位
453
+ * @param danmakuOpacity 弹幕不透明度(0-100)
454
+ * @returns 布局参数
455
+ */
456
+ function computeDanmakuLayout(height, danmakuArea, danmakuFontSize, danmakuOpacity) {
457
+ const fontScale = height / 1080;
458
+ const sizeConfig = FONT_SIZE_MAP[danmakuFontSize];
459
+ const fontSize = Math.round(sizeConfig.base * fontScale);
460
+ const trackH = Math.round(sizeConfig.trackH * fontScale);
461
+ const topMargin = Math.round(5 * fontScale);
462
+ const areaHeight = Math.floor(height * danmakuArea) - topMargin;
463
+ const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
464
+ const minGap = Math.round(15 * fontScale);
465
+ // libass 的 Fontsize 近似按点(pt)渲染,同名字号下字形约为 CSS px 的 3/4(实测 32 -> 288/381)
466
+ // 弹幕条要与之视觉对齐,CSS 字号按 0.75 换算
467
+ const stripFontSize = Math.max(1, Math.round(fontSize * 0.75));
468
+ // 将 0-100 的透明度转换为 ASS 的 alpha 值(0-255,0为不透明,255为完全透明)
469
+ const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55)
470
+ .toString(16)
471
+ .padStart(2, '0')
472
+ .toUpperCase();
473
+ return { fontSize, stripFontSize, trackH, topMargin, trackCount, minGap, alpha };
474
+ }
475
+ /**
476
+ * 计算同轨道上后一条弹幕与前一条弹幕的间距是否安全
477
+ * 间距指 next 头部进入屏幕右缘时与 prev 尾部的距离;若 next 更快,还会校验 prev 消失前不被追尾
478
+ * @param prev 先入轨(时间上更早)的弹幕
479
+ * @param next 后入轨(时间上更晚)的弹幕
480
+ * @param canvasWidth 画布宽度(像素)
481
+ * @param minGap 最小间距(像素)
482
+ * @returns 间距(像素),>= 0 表示不重叠
483
+ */
484
+ const calcTrackDistance = (prev, next, canvasWidth, minGap) => {
485
+ const prevSpeed = (canvasWidth + prev.textWidth) / prev.duration;
486
+ const nextSpeed = (canvasWidth + next.textWidth) / next.duration;
487
+ const elapsed = next.startTime - prev.startTime;
488
+ const prevRightX = canvasWidth - prevSpeed * elapsed + prev.textWidth;
489
+ let dist = canvasWidth - prevRightX - minGap;
490
+ if (nextSpeed > prevSpeed) {
491
+ const totalElapsed = next.startTime + next.duration - prev.startTime;
492
+ const prevRightXAtEnd = canvasWidth - prevSpeed * totalElapsed + prev.textWidth;
493
+ dist = Math.min(dist, -next.textWidth - prevRightXAtEnd - minGap);
494
+ }
495
+ return dist;
496
+ };
497
+ /**
498
+ * 单条弹幕入轨(区间表模型):每条轨道维护按时间排序的已承诺弹幕列表,
499
+ * 新弹幕只需与直接前驱/后继做间距检查(所有弹幕滚动时长相同,起止时间同序,最近邻居必是最强约束)
500
+ * @param lanes 所有轨道的区间表
501
+ * @param entry 待入轨弹幕
502
+ * @param canvasWidth 画布宽度(像素)
503
+ * @param minGap 同轨道相邻弹幕最小间距(像素)
504
+ * @param preferFreeLane true 时优先随机选择空闲轨道(用于点赞角标,使其散布到任意行);
505
+ * false 时优先紧凑复用(普通弹幕,保持"避免阶梯"的观感)
506
+ * @returns 轨道下标,放不下返回 -1
507
+ */
508
+ const allocateTrack = (lanes, entry, canvasWidth, minGap, preferFreeLane) => {
509
+ const { startTime, duration } = entry;
510
+ let bestIdx = -1;
511
+ let bestDist = Infinity;
512
+ const freeLanes = [];
513
+ for (let i = 0; i < lanes.length; i++) {
514
+ const lane = lanes[i];
515
+ // 找直接前驱(最后一个不晚于当前弹幕的已承诺条目;常见情况是追加在末尾,从后往前扫)
516
+ let predIdx = -1;
517
+ for (let j = lane.length - 1; j >= 0; j--) {
518
+ if (lane[j].startTime <= startTime) {
519
+ predIdx = j;
520
+ break;
521
+ }
522
+ }
523
+ const pred = predIdx >= 0 ? lane[predIdx] : null;
524
+ const succ = predIdx + 1 < lane.length ? lane[predIdx + 1] : null;
525
+ // 前驱仍在屏时必须保持间距;后继与当前弹幕时间交叠时,当前弹幕也不能挡住后继
526
+ const predActive = pred !== null && pred.startTime + pred.duration > startTime;
527
+ if (predActive && calcTrackDistance(pred, entry, canvasWidth, minGap) < 0)
528
+ continue;
529
+ if (succ !== null && startTime + duration > succ.startTime && calcTrackDistance(entry, succ, canvasWidth, minGap) < 0)
530
+ continue;
531
+ if (!predActive) {
532
+ // 当前时段空闲的轨道(含全空轨道),优先级最低,只有没有紧凑选择时才用
533
+ freeLanes.push(i);
534
+ continue;
535
+ }
536
+ // 紧凑排列:距离最小但仍 >= 0 的轨道,避免"阶梯"效果
537
+ const d = calcTrackDistance(pred, entry, canvasWidth, minGap);
538
+ if (d < bestDist) {
539
+ bestDist = d;
540
+ bestIdx = i;
541
+ }
542
+ }
543
+ if (preferFreeLane && freeLanes.length > 0)
544
+ return freeLanes[Math.floor(Math.random() * freeLanes.length)];
545
+ if (bestIdx !== -1)
546
+ return bestIdx;
547
+ if (freeLanes.length > 0)
548
+ return freeLanes[0];
549
+ return -1;
550
+ };
551
+ /**
552
+ * 把弹幕按时间序插入轨道区间表
553
+ * @param lane 目标轨道的区间表
554
+ * @param entry 待插入弹幕
555
+ */
556
+ const insertIntoLane = (lane, entry) => {
557
+ let j = lane.length - 1;
558
+ while (j >= 0 && lane[j].startTime > entry.startTime)
559
+ j--;
560
+ lane.splice(j + 1, 0, entry);
561
+ };
562
+ /**
563
+ * 弹幕滚动宽度基准:ASS 与 overlay 必须使用同一度量,否则两类弹幕速度不一致、会错开
564
+ * 纯文字用估算宽度;PNG 条取估算宽度与真实宽度的较大值(角标图标可能让真实宽度超过估算,
565
+ * 取大者保证条尾在 endTime 前完全滚出屏幕,不会突然消失)
566
+ * @param text 弹幕文本
567
+ * @param strip 预渲染弹幕条(纯文字弹幕为 undefined)
568
+ * @param fontSize ASS 字号(像素)
569
+ * @returns 滚动宽度基准(像素)
570
+ */
571
+ const resolveMoveWidth = (text, strip, fontSize) => {
572
+ return strip ? Math.max(estimateWidth(text, fontSize), strip.width) : estimateWidth(text, fontSize);
573
+ };
574
+ // ==================== ASS 生成 ====================
575
+ /**
576
+ * 生成 ASS 文件头(Script Info + 样式表 + 事件格式声明)
577
+ * @param width 画布宽度(ASS PlayResX)
578
+ * @param height 画布高度(ASS PlayResY)
579
+ * @param fontName 字体名
580
+ * @param layout 布局参数
581
+ * @returns ASS 头部内容
582
+ */
583
+ function buildAssHeader(width, height, fontName, layout) {
584
+ return `[Script Info]
585
+ Title: Douyin Danmaku
586
+ ScriptType: v4.00+
587
+ PlayResX: ${width}
588
+ PlayResY: ${height}
589
+ Timer: 100.0000
590
+
591
+ [V4+ Styles]
592
+ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
593
+ Style: Scroll,${fontName},${layout.fontSize},&H${layout.alpha}FFFFFF,&H${layout.alpha}FFFFFF,&H${layout.alpha}000000,&H${layout.alpha}000000,0,0,0,0,100,100,0,0,1,0.8,0,2,0,0,0,1
594
+
595
+ [Events]
596
+ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
597
+ `;
598
+ }
599
+ /**
600
+ * 生成一条纯文字弹幕的 ASS Dialogue 行(滚动弹幕,\move 从右缘外滚到左缘外)
601
+ * @param text 弹幕文本
602
+ * @param entry 轨道条目(时间/宽度基准)
603
+ * @param trackIdx 轨道下标
604
+ * @param canvasWidth 画布宽度(像素)
605
+ * @param layout 布局参数
606
+ * @returns Dialogue 行(含换行符)
607
+ */
608
+ function buildScrollDialogue(text, entry, trackIdx, canvasWidth, layout) {
609
+ const y = layout.topMargin + trackIdx * layout.trackH + layout.fontSize;
610
+ const content = escapeASS(text);
611
+ return `Dialogue: 0,${toASSTime(entry.startTime)},${toASSTime(entry.startTime + entry.duration)},Scroll,,0,0,0,,{\\an7}{\\move(${canvasWidth},${y},${-entry.textWidth},${y})}${content}\n`;
612
+ }
613
+ /**
614
+ * 生成一条图片弹幕的 overlay 描述
615
+ * @param strip 预渲染弹幕条
616
+ * @param entry 轨道条目(时间/宽度基准)
617
+ * @param trackIdx 轨道下标
618
+ * @param layout 布局参数
619
+ * @returns overlay 描述
620
+ */
621
+ function buildOverlay(strip, entry, trackIdx, layout) {
622
+ // ASS \an7 的 y 是文字顶部;条按视觉中心对齐文字中心
623
+ const textTop = layout.topMargin + trackIdx * layout.trackH + layout.fontSize;
624
+ const y = Math.max(0, Math.round(textTop + (layout.fontSize - strip.height) / 2));
625
+ return { pngPath: strip.pngPath, startTime: entry.startTime, endTime: entry.startTime + entry.duration, y, moveW: entry.textWidth };
626
+ }
627
+ /**
628
+ * 生成弹幕 ASS 与图片弹幕(表情/点赞)overlay 列表
629
+ * 纯文字弹幕输出 ASS \move 行;含表情或被选中展示点赞角标的弹幕预渲染为 PNG 条,统一参与轨道分配后转为 overlay
630
+ * @param danmakuList 弹幕列表
631
+ * @param width 画布宽度(像素)
632
+ * @param height 画布高度(像素)
633
+ * @param options 烧录配置
634
+ * @returns ASS 内容、overlay 列表、临时文件与分类统计
635
+ */
636
+ async function generateDouyinASS(danmakuList, width, height, options = {}) {
637
+ const { scrollTime = 8, danmakuOpacity = 70, fontName = 'Microsoft YaHei', danmakuArea = 0.5, danmakuFontSize = 'medium' } = options;
638
+ const layout = computeDanmakuLayout(height, danmakuArea, danmakuFontSize, danmakuOpacity);
639
+ // 过滤并排序弹幕
640
+ const sorted = danmakuList.filter((dm) => dm.text && dm.text.trim()).sort((a, b) => a.offset_time - b.offset_time);
641
+ const emojiList = await fetchDouyinEmojiList();
642
+ const liked = selectLikedDanmaku(sorted);
643
+ const stripStyle = { fontSize: layout.stripFontSize, fontName, opacity: Math.max(0, Math.min(100, danmakuOpacity)) / 100 };
644
+ const { strips, tempFiles } = await prepareDanmakuStrips(sorted, emojiList, liked, stripStyle);
645
+ const assLines = [];
646
+ const overlays = [];
647
+ let likedOverlayCount = 0;
648
+ let emojiOverlayCount = 0;
649
+ /**
650
+ * 弹幕入轨并上屏:写入 ASS(纯文字)或 overlay 列表(PNG 条)
651
+ * @param dm 弹幕元素
652
+ * @param preferFreeLane 是否优先随机选择空闲轨道(点赞角标弹幕为 true)
653
+ */
654
+ const placeDanmaku = (dm, preferFreeLane) => {
655
+ const strip = strips.get(dm.danmaku_id);
656
+ const entry = {
657
+ startTime: dm.offset_time,
658
+ duration: scrollTime * 1000,
659
+ textWidth: resolveMoveWidth(dm.text, strip, layout.fontSize)
660
+ };
661
+ const trackIdx = allocateTrack(lanes, entry, width, layout.minGap, preferFreeLane);
662
+ if (trackIdx === -1)
663
+ return;
664
+ insertIntoLane(lanes[trackIdx], entry);
665
+ if (strip) {
666
+ overlays.push(buildOverlay(strip, entry, trackIdx, layout));
667
+ if (liked.ids.has(dm.danmaku_id))
668
+ likedOverlayCount++;
669
+ else
670
+ emojiOverlayCount++;
671
+ return;
672
+ }
673
+ assLines.push(buildScrollDialogue(dm.text, entry, trackIdx, width, layout));
674
+ };
675
+ // 所有轨道共享,点赞角标弹幕按时间序先提交(获得优先级),普通弹幕后提交并绕开已承诺的角标。
676
+ // 弹幕密集时普通轨道会按比例丢弃弹幕,先提交保证被选中的高点赞弹幕不被丢弃;
677
+ // 角标优先随机选择空闲轨道,使其像普通弹幕一样散布在任意行
678
+ const lanes = Array.from({ length: layout.trackCount }, () => []);
679
+ // 第一遍:点赞角标弹幕(时间序),优先随机空闲轨道
680
+ for (const dm of sorted) {
681
+ if (liked.ids.has(dm.danmaku_id))
682
+ placeDanmaku(dm, true);
683
+ }
684
+ // 第二遍:普通弹幕(时间序),紧凑复用,绕开已承诺的角标弹幕
685
+ for (const dm of sorted) {
686
+ if (!liked.ids.has(dm.danmaku_id))
687
+ placeDanmaku(dm, false);
688
+ }
689
+ return {
690
+ ass: buildAssHeader(width, height, fontName, layout) + assLines.join(''),
691
+ overlays,
692
+ tempFiles,
693
+ stats: {
694
+ likedOverlays: likedOverlayCount,
695
+ emojiOverlays: emojiOverlayCount,
696
+ likedCandidates: liked.candidateCount,
697
+ likedTarget: liked.target
698
+ }
699
+ };
700
+ }
701
+ const MAX_OUTPUT_WIDTH = 2160;
702
+ /**
703
+ * 计算画布尺寸(竖屏适配)
704
+ * 抖音视频大多是竖屏,但也可能有横屏视频需要转竖屏
705
+ * @param origW 源视频宽度(像素)
706
+ * @param origH 源视频高度(像素)
707
+ * @param verticalMode 横屏转竖屏模式
708
+ * @returns 画布信息
709
+ */
710
+ function calcCanvas(origW, origH, verticalMode) {
711
+ if (verticalMode === 'off') {
712
+ return { width: origW, height: origH, offsetY: 0, isVertical: false };
713
+ }
714
+ const ratio = origW / origH;
715
+ const isWide = isLandscape(origW, origH);
716
+ if (verticalMode === 'force') {
717
+ const targetRatio = 16 / 9;
718
+ if (isWide) {
719
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
720
+ const newH = Math.round(newW * targetRatio);
721
+ const scaledH = Math.round(newW / ratio);
722
+ const offsetY = Math.round((newH - scaledH) / 2);
723
+ return { width: newW, height: newH, offsetY, isVertical: true, scale: newW / origW };
724
+ }
725
+ else {
726
+ const newW = Math.min(origW, MAX_OUTPUT_WIDTH);
727
+ const scaleRatio = newW / origW;
728
+ const scaledOrigH = Math.round(origH * scaleRatio);
729
+ const newH = Math.round(newW * targetRatio);
730
+ const offsetY = Math.round((newH - scaledOrigH) / 2);
731
+ return { width: newW, height: newH, offsetY: Math.max(0, offsetY), isVertical: true, scale: scaleRatio };
732
+ }
733
+ }
734
+ // standard 模式:只对宽屏视频进行转换
735
+ if (isWide && ratio >= 1.7) {
736
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
737
+ const scaleRatio = newW / origW;
738
+ const newH = Math.round(origW * scaleRatio);
739
+ const scaledH = Math.round(newW / ratio);
740
+ const offsetY = Math.round((newH - scaledH) / 2);
741
+ return { width: newW, height: newH, offsetY, isVertical: true, scale: newW / origW };
742
+ }
743
+ return { width: origW, height: origH, offsetY: 0, isVertical: false };
744
+ }
745
+ /**
746
+ * 构建 FFmpeg 滤镜(无图片弹幕时的 -vf 简单滤镜链)
747
+ * fps=60 放最前:视频帧复制(内容不变),libass 在每个新帧重新求值 \move,弹幕步进更小更顺滑
748
+ * @param canvas 画布信息
749
+ * @param assPath ASS 字幕文件路径
750
+ * @returns -vf 滤镜链字符串
751
+ */
752
+ function buildFilter(canvas, assPath) {
753
+ const escaped = escapeWinPath(assPath);
754
+ if (canvas.isVertical) {
755
+ if (canvas.scale && canvas.scale !== 1 && canvas.scale < 1) {
756
+ return `fps=${OUTPUT_FPS},scale=${canvas.width}:-1,pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
757
+ }
758
+ return `fps=${OUTPUT_FPS},pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
759
+ }
760
+ return `fps=${OUTPUT_FPS},subtitles='${escaped}'`;
761
+ }
762
+ /**
763
+ * 构建 FFmpeg filter_complex(含图片弹幕时的叠加链)
764
+ * 每条图片弹幕对应一个 PNG 输入(内容相同的弹幕条复用同一输入,见 burnDouyinDanmaku),
765
+ * 用 overlay 按 \move 同语义滚动:x(t) = width - (t - start) * speed,speed = (width + moveW) / scrollTime
766
+ * @param canvas 画布信息
767
+ * @param assPath ASS 字幕文件路径
768
+ * @param overlays 图片弹幕 overlay 列表
769
+ * @param inputIndices 每条 overlay 对应的 ffmpeg 输入序号(与去重后的 -i 顺序一致)
770
+ * @param scrollTime 滚动时间(秒)
771
+ * @returns filter_complex 字符串
772
+ */
773
+ function buildFilterComplex(canvas, assPath, overlays, inputIndices, scrollTime) {
774
+ const escaped = escapeWinPath(assPath);
775
+ let base = `[0:v]fps=${OUTPUT_FPS}`;
776
+ if (canvas.isVertical) {
777
+ if (canvas.scale && canvas.scale !== 1 && canvas.scale < 1) {
778
+ base += `,scale=${canvas.width}:-1`;
779
+ }
780
+ base += `,pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black`;
781
+ }
782
+ base += `,subtitles='${escaped}'[base]`;
783
+ const parts = [base];
784
+ let prev = 'base';
785
+ overlays.forEach((o, i) => {
786
+ const speed = ((canvas.width + o.moveW) / scrollTime).toFixed(3);
787
+ const s = (o.startTime / 1000).toFixed(3);
788
+ const e = (o.endTime / 1000).toFixed(3);
789
+ const label = i === overlays.length - 1 ? 'vout' : `v${i}`;
790
+ parts.push(`[${prev}][${inputIndices[i]}:v]overlay=x='${canvas.width}-(t-${s})*${speed}':y=${o.y}:enable='between(t,${s},${e})'[${label}]`);
791
+ prev = label;
792
+ });
793
+ return parts.join(';');
794
+ }
795
+ /**
796
+ * 生成相对路径转换器:能相对化的转相对路径(正斜杠),否则保留绝对路径
797
+ * 用于把 ffmpeg 命令行长度控制在 Windows cmd 的 8191 字符上限内
798
+ * @param workDir 工作目录(ffmpeg 的 cwd)
799
+ * @returns 路径转换函数
800
+ */
801
+ const createRelativizer = (workDir) => {
802
+ return (p) => {
803
+ const rel = node_path_1.default.relative(workDir, p).replace(/\\/g, '/');
804
+ return rel && !rel.startsWith('..') ? rel : p.replace(/\\/g, '/');
805
+ };
806
+ };
807
+ /**
808
+ * 规划图片弹幕的 ffmpeg 输入:内容相同的弹幕条复用同一个输入,减少命令行长度
809
+ * @param videoPath 视频文件路径(相对化后)
810
+ * @param overlays 图片弹幕 overlay 列表
811
+ * @param toRel 相对路径转换器
812
+ * @returns 输入参数列表与每条 overlay 的输入序号
813
+ */
814
+ function planOverlayInputs(videoPath, overlays, toRel) {
815
+ const inputIndexByPath = new Map();
816
+ const inputArgs = [`-i "${toRel(videoPath)}"`];
817
+ const inputIndices = overlays.map((o) => {
818
+ let idx = inputIndexByPath.get(o.pngPath);
819
+ if (idx === undefined) {
820
+ idx = inputIndexByPath.size + 1;
821
+ inputIndexByPath.set(o.pngPath, idx);
822
+ inputArgs.push(`-i "${toRel(o.pngPath)}"`);
823
+ }
824
+ return idx;
825
+ });
826
+ return { inputArgs, inputIndices };
827
+ }
828
+ /**
829
+ * 烧录抖音弹幕到视频
830
+ * @param videoPath 源视频文件路径
831
+ * @param danmakuList 弹幕列表
832
+ * @param outputPath 输出视频文件路径
833
+ * @param options 烧录配置
834
+ * @returns 是否烧录成功
835
+ */
836
+ async function burnDouyinDanmaku(videoPath, danmakuList, outputPath, options = {}) {
837
+ const { removeSource = false, verticalMode = 'off', videoCodec = 'h265', scrollTime = 8 } = options;
838
+ if (!node_fs_1.default.existsSync(videoPath)) {
839
+ node_karin_1.logger.error(`[DouyinDanmaku] 视频文件不存在: ${videoPath}`);
840
+ return false;
841
+ }
842
+ const resolution = await getDouyinResolution(videoPath);
843
+ const sourceBitrate = await getVideoBitrate(videoPath);
844
+ const canvas = calcCanvas(resolution.width, resolution.height, verticalMode);
845
+ if (canvas.isVertical) {
846
+ node_karin_1.logger.debug(`[DouyinDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
847
+ }
848
+ node_karin_1.logger.debug(`[DouyinDanmaku] 分辨率: ${canvas.width}x${canvas.height}, 码率: ${sourceBitrate}kbps`);
849
+ // 生成 ASS 与图片弹幕 overlay(使用画布尺寸)
850
+ const { ass, overlays, tempFiles, stats } = await generateDouyinASS(danmakuList, canvas.width, canvas.height, options);
851
+ const assPath = videoPath.replace(/\.[^.]+$/, '_danmaku.ass');
852
+ node_fs_1.default.writeFileSync(assPath, ass, 'utf-8');
853
+ node_karin_1.logger.debug(`[DouyinDanmaku] 弹幕字幕已生成: ${assPath},共 ${danmakuList.length} 条,` +
854
+ `图片弹幕 ${overlays.length} 条(点赞角标 ${stats.likedOverlays} 条 [候选 ${stats.likedCandidates}/目标 ${stats.likedTarget}],表情 ${stats.emojiOverlays} 条)`);
855
+ // 编码(使用原视频码率作为目标)
856
+ const encoder = await detectEncoder(videoCodec);
857
+ const encoderParams = getEncoderParams(encoder, sourceBitrate);
858
+ let command;
859
+ let execCwd;
860
+ if (overlays.length === 0) {
861
+ const filter = buildFilter(canvas, assPath);
862
+ command = `-y -i "${videoPath}" -vf "${filter}" -r ${OUTPUT_FPS} ${encoderParams} -c:a copy "${outputPath}"`;
863
+ }
864
+ else {
865
+ // PNG 是单帧输入,overlay 默认 eof_action=repeat 会保持最后一帧,无需 -loop(looped 输入会导致编码无法终止)
866
+ // 图片弹幕多的时候命令行会超过 Windows cmd 的 8191 字符上限,因此:
867
+ // 1. 以视频所在目录为 cwd,所有路径转相对路径
868
+ // 2. 内容相同的弹幕条复用同一个输入
869
+ // 3. filter_complex 写入脚本文件,用 -filter_complex_script 引用
870
+ const workDir = node_path_1.default.dirname(videoPath);
871
+ execCwd = workDir;
872
+ const toRel = createRelativizer(workDir);
873
+ const { inputArgs, inputIndices } = planOverlayInputs(videoPath, overlays, toRel);
874
+ const graphPath = videoPath.replace(/\.[^.]+$/, '_danmaku.graph.txt');
875
+ node_fs_1.default.writeFileSync(graphPath, buildFilterComplex(canvas, toRel(assPath), overlays, inputIndices, scrollTime), 'utf-8');
876
+ tempFiles.push(graphPath);
877
+ command = `-y ${inputArgs.join(' ')} -filter_complex_script "${toRel(graphPath)}" -map "[vout]" -map "0:a?" -r ${OUTPUT_FPS} ${encoderParams} -c:a copy "${toRel(outputPath)}"`;
878
+ }
879
+ const result = await (0, node_karin_1.ffmpeg)(command, execCwd ? { cwd: execCwd } : undefined);
880
+ utils_1.Common.removeFile(assPath, true);
881
+ for (const file of tempFiles) {
882
+ utils_1.Common.removeFile(file, true);
883
+ }
884
+ if (result.status) {
885
+ node_karin_1.logger.mark(`[DouyinDanmaku] 弹幕烧录成功: ${outputPath}`);
886
+ if (removeSource)
887
+ utils_1.Common.removeFile(videoPath);
888
+ }
889
+ else {
890
+ node_karin_1.logger.error('[DouyinDanmaku] 弹幕烧录失败', result);
891
+ }
892
+ return result.status;
893
+ }