@jackwener/opencli 1.4.0 → 1.5.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 (514) hide show
  1. package/.github/actions/setup-chrome/action.yml +5 -4
  2. package/.github/workflows/build-extension.yml +2 -6
  3. package/.github/workflows/ci.yml +37 -3
  4. package/.github/workflows/e2e-headed.yml +16 -3
  5. package/CHANGELOG.md +23 -0
  6. package/PRIVACY.md +57 -0
  7. package/README.md +36 -7
  8. package/README.zh-CN.md +13 -6
  9. package/SKILL.md +103 -2
  10. package/dist/browser/cdp.d.ts +2 -1
  11. package/dist/browser/discover.d.ts +4 -1
  12. package/dist/browser/discover.js +6 -2
  13. package/dist/browser/errors.d.ts +2 -2
  14. package/dist/browser/errors.js +4 -12
  15. package/dist/browser/mcp.d.ts +2 -1
  16. package/dist/build-manifest.d.ts +2 -0
  17. package/dist/build-manifest.js +39 -14
  18. package/dist/build-manifest.test.js +21 -0
  19. package/dist/capabilityRouting.d.ts +2 -0
  20. package/dist/capabilityRouting.js +2 -1
  21. package/dist/cli-manifest.json +1838 -151
  22. package/dist/cli.js +34 -3
  23. package/dist/clis/36kr/article.d.ts +1 -0
  24. package/dist/clis/36kr/article.js +62 -0
  25. package/dist/clis/36kr/hot.d.ts +3 -0
  26. package/dist/clis/36kr/hot.js +80 -0
  27. package/dist/clis/36kr/hot.test.d.ts +1 -0
  28. package/dist/clis/36kr/hot.test.js +15 -0
  29. package/dist/clis/36kr/news.d.ts +1 -0
  30. package/dist/clis/36kr/news.js +51 -0
  31. package/dist/clis/36kr/news.test.d.ts +1 -0
  32. package/dist/clis/36kr/news.test.js +85 -0
  33. package/dist/clis/36kr/search.d.ts +1 -0
  34. package/dist/clis/36kr/search.js +72 -0
  35. package/dist/clis/apple-podcasts/search.js +2 -1
  36. package/dist/clis/arxiv/search.js +2 -2
  37. package/dist/clis/bbc/news.js +0 -1
  38. package/dist/clis/bilibili/comments.d.ts +5 -0
  39. package/dist/clis/bilibili/comments.js +40 -0
  40. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  41. package/dist/clis/bilibili/comments.test.js +82 -0
  42. package/dist/clis/chatgpt/ask.js +29 -14
  43. package/dist/clis/chatgpt/ax.d.ts +6 -0
  44. package/dist/clis/chatgpt/ax.js +172 -1
  45. package/dist/clis/chatgpt/model.d.ts +1 -0
  46. package/dist/clis/chatgpt/model.js +24 -0
  47. package/dist/clis/chatgpt/send.js +12 -3
  48. package/dist/clis/ctrip/search.js +0 -1
  49. package/dist/clis/douban/download.d.ts +1 -0
  50. package/dist/clis/douban/download.js +67 -0
  51. package/dist/clis/douban/download.test.d.ts +1 -0
  52. package/dist/clis/douban/download.test.js +170 -0
  53. package/dist/clis/douban/photos.d.ts +1 -0
  54. package/dist/clis/douban/photos.js +34 -0
  55. package/dist/clis/douban/utils.d.ts +25 -0
  56. package/dist/clis/douban/utils.js +190 -1
  57. package/dist/clis/douban/utils.test.d.ts +1 -0
  58. package/dist/clis/douban/utils.test.js +64 -0
  59. package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
  60. package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
  61. package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
  62. package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
  63. package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
  64. package/dist/clis/douyin/_shared/creation-id.js +5 -0
  65. package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
  66. package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
  67. package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
  68. package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
  69. package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
  70. package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
  71. package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
  72. package/dist/clis/douyin/_shared/sts2.js +15 -0
  73. package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
  74. package/dist/clis/douyin/_shared/text-extra.js +15 -0
  75. package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
  76. package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
  77. package/dist/clis/douyin/_shared/timing.d.ts +2 -0
  78. package/dist/clis/douyin/_shared/timing.js +22 -0
  79. package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
  80. package/dist/clis/douyin/_shared/timing.test.js +28 -0
  81. package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
  82. package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
  83. package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
  84. package/dist/clis/douyin/_shared/tos-upload.js +295 -0
  85. package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
  86. package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
  87. package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
  88. package/dist/clis/douyin/_shared/transcode.js +45 -0
  89. package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
  90. package/dist/clis/douyin/_shared/transcode.test.js +93 -0
  91. package/dist/clis/douyin/_shared/types.d.ts +26 -0
  92. package/dist/clis/douyin/_shared/types.js +1 -0
  93. package/dist/clis/douyin/activities.d.ts +1 -0
  94. package/dist/clis/douyin/activities.js +20 -0
  95. package/dist/clis/douyin/activities.test.d.ts +1 -0
  96. package/dist/clis/douyin/activities.test.js +22 -0
  97. package/dist/clis/douyin/collections.d.ts +1 -0
  98. package/dist/clis/douyin/collections.js +22 -0
  99. package/dist/clis/douyin/collections.test.d.ts +1 -0
  100. package/dist/clis/douyin/collections.test.js +23 -0
  101. package/dist/clis/douyin/delete.d.ts +1 -0
  102. package/dist/clis/douyin/delete.js +18 -0
  103. package/dist/clis/douyin/delete.test.d.ts +1 -0
  104. package/dist/clis/douyin/delete.test.js +11 -0
  105. package/dist/clis/douyin/draft.d.ts +14 -0
  106. package/dist/clis/douyin/draft.js +237 -0
  107. package/dist/clis/douyin/draft.test.d.ts +1 -0
  108. package/dist/clis/douyin/draft.test.js +11 -0
  109. package/dist/clis/douyin/drafts.d.ts +1 -0
  110. package/dist/clis/douyin/drafts.js +23 -0
  111. package/dist/clis/douyin/drafts.test.d.ts +1 -0
  112. package/dist/clis/douyin/drafts.test.js +11 -0
  113. package/dist/clis/douyin/hashtag.d.ts +1 -0
  114. package/dist/clis/douyin/hashtag.js +45 -0
  115. package/dist/clis/douyin/hashtag.test.d.ts +1 -0
  116. package/dist/clis/douyin/hashtag.test.js +25 -0
  117. package/dist/clis/douyin/location.d.ts +1 -0
  118. package/dist/clis/douyin/location.js +24 -0
  119. package/dist/clis/douyin/location.test.d.ts +1 -0
  120. package/dist/clis/douyin/location.test.js +23 -0
  121. package/dist/clis/douyin/profile.d.ts +1 -0
  122. package/dist/clis/douyin/profile.js +28 -0
  123. package/dist/clis/douyin/profile.test.d.ts +1 -0
  124. package/dist/clis/douyin/profile.test.js +11 -0
  125. package/dist/clis/douyin/publish.d.ts +14 -0
  126. package/dist/clis/douyin/publish.js +288 -0
  127. package/dist/clis/douyin/publish.test.d.ts +1 -0
  128. package/dist/clis/douyin/publish.test.js +38 -0
  129. package/dist/clis/douyin/stats.d.ts +1 -0
  130. package/dist/clis/douyin/stats.js +27 -0
  131. package/dist/clis/douyin/stats.test.d.ts +1 -0
  132. package/dist/clis/douyin/stats.test.js +22 -0
  133. package/dist/clis/douyin/update.d.ts +1 -0
  134. package/dist/clis/douyin/update.js +31 -0
  135. package/dist/clis/douyin/update.test.d.ts +1 -0
  136. package/dist/clis/douyin/update.test.js +11 -0
  137. package/dist/clis/douyin/videos.d.ts +1 -0
  138. package/dist/clis/douyin/videos.js +34 -0
  139. package/dist/clis/douyin/videos.test.d.ts +1 -0
  140. package/dist/clis/douyin/videos.test.js +11 -0
  141. package/dist/clis/hackernews/search.yaml +1 -1
  142. package/dist/clis/imdb/person.d.ts +1 -0
  143. package/dist/clis/imdb/person.js +203 -0
  144. package/dist/clis/imdb/reviews.d.ts +1 -0
  145. package/dist/clis/imdb/reviews.js +88 -0
  146. package/dist/clis/imdb/search.d.ts +1 -0
  147. package/dist/clis/imdb/search.js +161 -0
  148. package/dist/clis/imdb/title.d.ts +1 -0
  149. package/dist/clis/imdb/title.js +93 -0
  150. package/dist/clis/imdb/top.d.ts +1 -0
  151. package/dist/clis/imdb/top.js +53 -0
  152. package/dist/clis/imdb/trending.d.ts +1 -0
  153. package/dist/clis/imdb/trending.js +52 -0
  154. package/dist/clis/imdb/utils.d.ts +46 -0
  155. package/dist/clis/imdb/utils.js +285 -0
  156. package/dist/clis/imdb/utils.test.d.ts +1 -0
  157. package/dist/clis/imdb/utils.test.js +88 -0
  158. package/dist/clis/instagram/search.yaml +2 -1
  159. package/dist/clis/jd/item.d.ts +4 -0
  160. package/dist/clis/jd/item.js +16 -15
  161. package/dist/clis/jd/item.test.js +16 -1
  162. package/dist/clis/linux-do/categories.yaml +38 -9
  163. package/dist/clis/linux-do/category.d.ts +1 -0
  164. package/dist/clis/linux-do/category.js +36 -0
  165. package/dist/clis/linux-do/feed.d.ts +45 -0
  166. package/dist/clis/linux-do/feed.js +397 -0
  167. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  168. package/dist/clis/linux-do/feed.test.js +118 -0
  169. package/dist/clis/linux-do/hot.d.ts +1 -0
  170. package/dist/clis/linux-do/hot.js +25 -0
  171. package/dist/clis/linux-do/latest.d.ts +1 -0
  172. package/dist/clis/linux-do/latest.js +18 -0
  173. package/dist/clis/linux-do/search.yaml +3 -1
  174. package/dist/clis/linux-do/tags.yaml +41 -0
  175. package/dist/clis/linux-do/topic.yaml +41 -3
  176. package/dist/clis/linux-do/user-posts.yaml +67 -0
  177. package/dist/clis/linux-do/user-topics.yaml +54 -0
  178. package/dist/clis/medium/search.js +1 -1
  179. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  180. package/dist/clis/paperreview/commands.test.js +243 -0
  181. package/dist/clis/paperreview/feedback.d.ts +1 -0
  182. package/dist/clis/paperreview/feedback.js +52 -0
  183. package/dist/clis/paperreview/review.d.ts +1 -0
  184. package/dist/clis/paperreview/review.js +37 -0
  185. package/dist/clis/paperreview/submit.d.ts +1 -0
  186. package/dist/clis/paperreview/submit.js +85 -0
  187. package/dist/clis/paperreview/utils.d.ts +46 -0
  188. package/dist/clis/paperreview/utils.js +197 -0
  189. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  190. package/dist/clis/paperreview/utils.test.js +49 -0
  191. package/dist/clis/producthunt/browse.d.ts +1 -0
  192. package/dist/clis/producthunt/browse.js +99 -0
  193. package/dist/clis/producthunt/hot.d.ts +1 -0
  194. package/dist/clis/producthunt/hot.js +110 -0
  195. package/dist/clis/producthunt/posts.d.ts +1 -0
  196. package/dist/clis/producthunt/posts.js +28 -0
  197. package/dist/clis/producthunt/today.d.ts +1 -0
  198. package/dist/clis/producthunt/today.js +35 -0
  199. package/dist/clis/producthunt/utils.d.ts +29 -0
  200. package/dist/clis/producthunt/utils.js +99 -0
  201. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  202. package/dist/clis/producthunt/utils.test.js +64 -0
  203. package/dist/clis/reuters/search.js +0 -1
  204. package/dist/clis/twitter/article.js +4 -28
  205. package/dist/clis/twitter/likes.d.ts +24 -0
  206. package/dist/clis/twitter/likes.js +217 -0
  207. package/dist/clis/twitter/likes.test.d.ts +1 -0
  208. package/dist/clis/twitter/likes.test.js +85 -0
  209. package/dist/clis/twitter/profile.js +4 -28
  210. package/dist/clis/twitter/search.js +7 -4
  211. package/dist/clis/twitter/search.test.js +56 -2
  212. package/dist/clis/twitter/shared.d.ts +6 -0
  213. package/dist/clis/twitter/shared.js +35 -0
  214. package/dist/clis/twitter/timeline.js +2 -13
  215. package/dist/clis/weibo/comments.d.ts +1 -0
  216. package/dist/clis/weibo/comments.js +53 -0
  217. package/dist/clis/weibo/feed.d.ts +1 -0
  218. package/dist/clis/weibo/feed.js +56 -0
  219. package/dist/clis/weibo/hot.js +0 -1
  220. package/dist/clis/weibo/me.d.ts +1 -0
  221. package/dist/clis/weibo/me.js +76 -0
  222. package/dist/clis/weibo/post.d.ts +1 -0
  223. package/dist/clis/weibo/post.js +75 -0
  224. package/dist/clis/weibo/user.d.ts +1 -0
  225. package/dist/clis/weibo/user.js +63 -0
  226. package/dist/clis/weibo/utils.d.ts +6 -0
  227. package/dist/clis/weibo/utils.js +30 -0
  228. package/dist/clis/weixin/download.d.ts +17 -0
  229. package/dist/clis/weixin/download.js +88 -20
  230. package/dist/clis/weread/book.js +2 -2
  231. package/dist/clis/weread/commands.test.d.ts +3 -0
  232. package/dist/clis/weread/commands.test.js +43 -0
  233. package/dist/clis/weread/highlights.js +2 -2
  234. package/dist/clis/weread/notebooks.js +2 -2
  235. package/dist/clis/weread/notes.js +3 -3
  236. package/dist/clis/weread/search.js +3 -2
  237. package/dist/clis/weread/shelf.js +2 -2
  238. package/dist/clis/weread/utils.d.ts +4 -4
  239. package/dist/clis/weread/utils.js +32 -14
  240. package/dist/clis/weread/utils.test.js +1 -28
  241. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  242. package/dist/clis/xiaohongshu/comments.js +74 -0
  243. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  244. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  245. package/dist/clis/xiaohongshu/publish.js +114 -18
  246. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  247. package/dist/clis/xiaohongshu/publish.test.js +119 -0
  248. package/dist/clis/xueqiu/search.yaml +2 -1
  249. package/dist/clis/yahoo-finance/quote.js +0 -1
  250. package/dist/clis/youtube/channel.d.ts +1 -0
  251. package/dist/clis/youtube/channel.js +150 -0
  252. package/dist/clis/youtube/comments.d.ts +1 -0
  253. package/dist/clis/youtube/comments.js +95 -0
  254. package/dist/clis/youtube/search.js +0 -1
  255. package/dist/clis/zhihu/search.yaml +2 -1
  256. package/dist/commanderAdapter.d.ts +1 -0
  257. package/dist/commanderAdapter.js +176 -29
  258. package/dist/commanderAdapter.test.d.ts +1 -0
  259. package/dist/commanderAdapter.test.js +62 -0
  260. package/dist/daemon.js +17 -1
  261. package/dist/discovery.js +8 -14
  262. package/dist/doctor.d.ts +1 -0
  263. package/dist/doctor.js +9 -2
  264. package/dist/download/index.js +63 -51
  265. package/dist/download/index.test.js +17 -4
  266. package/dist/errors.d.ts +3 -1
  267. package/dist/errors.js +15 -32
  268. package/dist/execution.d.ts +1 -3
  269. package/dist/execution.js +21 -1
  270. package/dist/external-clis.yaml +0 -17
  271. package/dist/hooks.js +2 -0
  272. package/dist/main.js +5 -0
  273. package/dist/output.js +5 -1
  274. package/dist/pipeline/executor.js +3 -4
  275. package/dist/plugin-manifest.d.ts +70 -0
  276. package/dist/plugin-manifest.js +160 -0
  277. package/dist/plugin-manifest.test.d.ts +4 -0
  278. package/dist/plugin-manifest.test.js +179 -0
  279. package/dist/plugin.d.ts +38 -5
  280. package/dist/plugin.js +267 -33
  281. package/dist/plugin.test.js +220 -3
  282. package/dist/registry.d.ts +4 -0
  283. package/dist/registry.js +2 -0
  284. package/dist/runtime-detect.d.ts +21 -0
  285. package/dist/runtime-detect.js +32 -0
  286. package/dist/runtime-detect.test.d.ts +1 -0
  287. package/dist/runtime-detect.test.js +27 -0
  288. package/dist/runtime.js +1 -1
  289. package/dist/serialization.d.ts +2 -0
  290. package/dist/serialization.js +6 -0
  291. package/dist/types.d.ts +1 -0
  292. package/dist/update-check.d.ts +22 -0
  293. package/dist/update-check.js +112 -0
  294. package/dist/weixin-download.test.d.ts +1 -0
  295. package/dist/weixin-download.test.js +30 -0
  296. package/dist/weread-private-api-regression.test.d.ts +1 -0
  297. package/dist/weread-private-api-regression.test.js +122 -0
  298. package/dist/weread-search-regression.test.d.ts +1 -0
  299. package/dist/weread-search-regression.test.js +39 -0
  300. package/dist/yaml-schema.d.ts +3 -0
  301. package/dist/yaml-schema.js +18 -1
  302. package/docs/.vitepress/config.mts +17 -0
  303. package/docs/adapters/browser/36kr.md +47 -0
  304. package/docs/adapters/browser/douban.md +14 -0
  305. package/docs/adapters/browser/douyin.md +75 -0
  306. package/docs/adapters/browser/imdb.md +47 -0
  307. package/docs/adapters/browser/jd.md +2 -2
  308. package/docs/adapters/browser/linux-do.md +181 -20
  309. package/docs/adapters/browser/paperreview.md +43 -0
  310. package/docs/adapters/browser/producthunt.md +49 -0
  311. package/docs/adapters/browser/twitter.md +6 -0
  312. package/docs/adapters/desktop/chatgpt.md +5 -0
  313. package/docs/adapters/index.md +12 -3
  314. package/docs/advanced/download.md +4 -0
  315. package/docs/advanced/rate-limiter-plugin.md +99 -0
  316. package/docs/guide/electron-app-cli.md +200 -0
  317. package/docs/guide/getting-started.md +1 -0
  318. package/docs/guide/plugins.md +87 -0
  319. package/docs/zh/guide/electron-app-cli.md +188 -0
  320. package/docs/zh/guide/getting-started.md +1 -0
  321. package/docs/zh/guide/plugins.md +65 -0
  322. package/extension/dist/background.js +508 -518
  323. package/extension/manifest.json +6 -2
  324. package/extension/package.json +2 -1
  325. package/extension/popup.html +84 -0
  326. package/extension/popup.js +25 -0
  327. package/extension/scripts/package-release.mjs +179 -0
  328. package/extension/src/background.ts +22 -1
  329. package/package.json +4 -1
  330. package/scripts/postinstall.js +10 -0
  331. package/src/browser/cdp.ts +2 -1
  332. package/src/browser/discover.ts +8 -3
  333. package/src/browser/errors.ts +13 -14
  334. package/src/browser/mcp.ts +2 -1
  335. package/src/build-manifest.test.ts +23 -0
  336. package/src/build-manifest.ts +40 -15
  337. package/src/capabilityRouting.ts +2 -1
  338. package/src/cli.ts +35 -3
  339. package/src/clis/36kr/article.ts +69 -0
  340. package/src/clis/36kr/hot.test.ts +19 -0
  341. package/src/clis/36kr/hot.ts +100 -0
  342. package/src/clis/36kr/news.test.ts +90 -0
  343. package/src/clis/36kr/news.ts +54 -0
  344. package/src/clis/36kr/search.ts +78 -0
  345. package/src/clis/apple-podcasts/search.ts +2 -1
  346. package/src/clis/arxiv/search.ts +2 -2
  347. package/src/clis/bbc/news.ts +0 -1
  348. package/src/clis/bilibili/comments.test.ts +102 -0
  349. package/src/clis/bilibili/comments.ts +44 -0
  350. package/src/clis/chatgpt/ask.ts +28 -14
  351. package/src/clis/chatgpt/ax.ts +180 -1
  352. package/src/clis/chatgpt/model.ts +27 -0
  353. package/src/clis/chatgpt/send.ts +16 -6
  354. package/src/clis/ctrip/search.ts +0 -1
  355. package/src/clis/douban/download.test.ts +196 -0
  356. package/src/clis/douban/download.ts +78 -0
  357. package/src/clis/douban/photos.ts +36 -0
  358. package/src/clis/douban/utils.test.ts +97 -0
  359. package/src/clis/douban/utils.ts +232 -1
  360. package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
  361. package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
  362. package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
  363. package/src/clis/douyin/_shared/creation-id.ts +8 -0
  364. package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
  365. package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
  366. package/src/clis/douyin/_shared/sts2.ts +20 -0
  367. package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
  368. package/src/clis/douyin/_shared/text-extra.ts +33 -0
  369. package/src/clis/douyin/_shared/timing.test.ts +38 -0
  370. package/src/clis/douyin/_shared/timing.ts +22 -0
  371. package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
  372. package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
  373. package/src/clis/douyin/_shared/tos-upload.ts +444 -0
  374. package/src/clis/douyin/_shared/transcode.test.ts +117 -0
  375. package/src/clis/douyin/_shared/transcode.ts +78 -0
  376. package/src/clis/douyin/_shared/types.ts +29 -0
  377. package/src/clis/douyin/activities.test.ts +25 -0
  378. package/src/clis/douyin/activities.ts +23 -0
  379. package/src/clis/douyin/collections.test.ts +26 -0
  380. package/src/clis/douyin/collections.ts +25 -0
  381. package/src/clis/douyin/delete.test.ts +12 -0
  382. package/src/clis/douyin/delete.ts +20 -0
  383. package/src/clis/douyin/draft.test.ts +12 -0
  384. package/src/clis/douyin/draft.ts +282 -0
  385. package/src/clis/douyin/drafts.test.ts +12 -0
  386. package/src/clis/douyin/drafts.ts +27 -0
  387. package/src/clis/douyin/hashtag.test.ts +28 -0
  388. package/src/clis/douyin/hashtag.ts +56 -0
  389. package/src/clis/douyin/location.test.ts +26 -0
  390. package/src/clis/douyin/location.ts +27 -0
  391. package/src/clis/douyin/profile.test.ts +12 -0
  392. package/src/clis/douyin/profile.ts +37 -0
  393. package/src/clis/douyin/publish.test.ts +45 -0
  394. package/src/clis/douyin/publish.ts +340 -0
  395. package/src/clis/douyin/stats.test.ts +25 -0
  396. package/src/clis/douyin/stats.ts +30 -0
  397. package/src/clis/douyin/update.test.ts +12 -0
  398. package/src/clis/douyin/update.ts +43 -0
  399. package/src/clis/douyin/videos.test.ts +12 -0
  400. package/src/clis/douyin/videos.ts +49 -0
  401. package/src/clis/hackernews/search.yaml +1 -1
  402. package/src/clis/imdb/person.ts +232 -0
  403. package/src/clis/imdb/reviews.ts +111 -0
  404. package/src/clis/imdb/search.ts +179 -0
  405. package/src/clis/imdb/title.ts +121 -0
  406. package/src/clis/imdb/top.ts +67 -0
  407. package/src/clis/imdb/trending.ts +66 -0
  408. package/src/clis/imdb/utils.test.ts +117 -0
  409. package/src/clis/imdb/utils.ts +305 -0
  410. package/src/clis/instagram/search.yaml +2 -1
  411. package/src/clis/jd/item.test.ts +18 -1
  412. package/src/clis/jd/item.ts +18 -15
  413. package/src/clis/linux-do/categories.yaml +38 -9
  414. package/src/clis/linux-do/category.ts +37 -0
  415. package/src/clis/linux-do/feed.test.ts +132 -0
  416. package/src/clis/linux-do/feed.ts +501 -0
  417. package/src/clis/linux-do/hot.ts +26 -0
  418. package/src/clis/linux-do/latest.ts +19 -0
  419. package/src/clis/linux-do/search.yaml +3 -1
  420. package/src/clis/linux-do/tags.yaml +41 -0
  421. package/src/clis/linux-do/topic.yaml +41 -3
  422. package/src/clis/linux-do/user-posts.yaml +67 -0
  423. package/src/clis/linux-do/user-topics.yaml +54 -0
  424. package/src/clis/medium/search.ts +1 -1
  425. package/src/clis/paperreview/commands.test.ts +283 -0
  426. package/src/clis/paperreview/feedback.ts +64 -0
  427. package/src/clis/paperreview/review.ts +47 -0
  428. package/src/clis/paperreview/submit.ts +119 -0
  429. package/src/clis/paperreview/utils.test.ts +68 -0
  430. package/src/clis/paperreview/utils.ts +276 -0
  431. package/src/clis/producthunt/browse.ts +109 -0
  432. package/src/clis/producthunt/hot.ts +127 -0
  433. package/src/clis/producthunt/posts.ts +29 -0
  434. package/src/clis/producthunt/today.ts +37 -0
  435. package/src/clis/producthunt/utils.test.ts +72 -0
  436. package/src/clis/producthunt/utils.ts +122 -0
  437. package/src/clis/reuters/search.ts +0 -1
  438. package/src/clis/twitter/article.ts +5 -28
  439. package/src/clis/twitter/likes.test.ts +91 -0
  440. package/src/clis/twitter/likes.ts +256 -0
  441. package/src/clis/twitter/profile.ts +5 -28
  442. package/src/clis/twitter/search.test.ts +71 -2
  443. package/src/clis/twitter/search.ts +8 -4
  444. package/src/clis/twitter/shared.ts +45 -0
  445. package/src/clis/twitter/timeline.ts +2 -13
  446. package/src/clis/weibo/comments.ts +54 -0
  447. package/src/clis/weibo/feed.ts +57 -0
  448. package/src/clis/weibo/hot.ts +0 -1
  449. package/src/clis/weibo/me.ts +77 -0
  450. package/src/clis/weibo/post.ts +77 -0
  451. package/src/clis/weibo/user.ts +64 -0
  452. package/src/clis/weibo/utils.ts +32 -0
  453. package/src/clis/weixin/download.ts +114 -20
  454. package/src/clis/weread/book.ts +2 -2
  455. package/src/clis/weread/commands.test.ts +57 -0
  456. package/src/clis/weread/highlights.ts +2 -2
  457. package/src/clis/weread/notebooks.ts +2 -2
  458. package/src/clis/weread/notes.ts +3 -3
  459. package/src/clis/weread/search.ts +3 -2
  460. package/src/clis/weread/shelf.ts +2 -2
  461. package/src/clis/weread/utils.test.ts +1 -32
  462. package/src/clis/weread/utils.ts +41 -16
  463. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  464. package/src/clis/xiaohongshu/comments.ts +81 -0
  465. package/src/clis/xiaohongshu/publish.test.ts +137 -0
  466. package/src/clis/xiaohongshu/publish.ts +129 -18
  467. package/src/clis/xueqiu/search.yaml +2 -1
  468. package/src/clis/yahoo-finance/quote.ts +0 -1
  469. package/src/clis/youtube/channel.ts +155 -0
  470. package/src/clis/youtube/comments.ts +97 -0
  471. package/src/clis/youtube/search.ts +0 -1
  472. package/src/clis/zhihu/search.yaml +2 -1
  473. package/src/commanderAdapter.test.ts +78 -0
  474. package/src/commanderAdapter.ts +188 -24
  475. package/src/daemon.ts +19 -1
  476. package/src/discovery.ts +8 -15
  477. package/src/doctor.ts +13 -2
  478. package/src/download/index.test.ts +14 -4
  479. package/src/download/index.ts +67 -55
  480. package/src/errors.ts +25 -66
  481. package/src/execution.ts +28 -3
  482. package/src/external-clis.yaml +0 -17
  483. package/src/hooks.ts +1 -0
  484. package/src/main.ts +6 -0
  485. package/src/output.ts +3 -1
  486. package/src/pipeline/executor.ts +4 -6
  487. package/src/plugin-manifest.test.ts +223 -0
  488. package/src/plugin-manifest.ts +206 -0
  489. package/src/plugin.test.ts +246 -2
  490. package/src/plugin.ts +338 -36
  491. package/src/registry.ts +6 -1
  492. package/src/runtime-detect.test.ts +30 -0
  493. package/src/runtime-detect.ts +36 -0
  494. package/src/runtime.ts +1 -1
  495. package/src/serialization.ts +4 -0
  496. package/src/types.ts +1 -0
  497. package/src/update-check.ts +114 -0
  498. package/src/weixin-download.test.ts +64 -0
  499. package/src/weread-private-api-regression.test.ts +150 -0
  500. package/src/weread-search-regression.test.ts +44 -0
  501. package/src/yaml-schema.ts +20 -0
  502. package/tests/e2e/browser-auth.test.ts +13 -9
  503. package/tests/e2e/browser-public-extended.test.ts +162 -0
  504. package/tests/e2e/browser-public.test.ts +55 -136
  505. package/tests/e2e/helpers.ts +2 -1
  506. package/tests/e2e/public-commands.test.ts +37 -3
  507. package/tests/smoke/api-health.test.ts +1 -1
  508. package/vitest.config.ts +34 -17
  509. package/dist/clis/linux-do/category.yaml +0 -51
  510. package/dist/clis/linux-do/hot.yaml +0 -50
  511. package/dist/clis/linux-do/latest.yaml +0 -40
  512. package/src/clis/linux-do/category.yaml +0 -51
  513. package/src/clis/linux-do/hot.yaml +0 -50
  514. package/src/clis/linux-do/latest.yaml +0 -40
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { IPage } from '../../types.js';
3
+ import {
4
+ getDoubanPhotoExtension,
5
+ loadDoubanSubjectPhotos,
6
+ normalizeDoubanSubjectId,
7
+ promoteDoubanPhotoUrl,
8
+ resolveDoubanPhotoAssetUrl,
9
+ } from './utils.js';
10
+
11
+ describe('douban utils', () => {
12
+ it('normalizes valid subject ids', () => {
13
+ expect(normalizeDoubanSubjectId(' 30382501 ')).toBe('30382501');
14
+ });
15
+
16
+ it('rejects invalid subject ids', () => {
17
+ expect(() => normalizeDoubanSubjectId('tt30382501')).toThrow('Invalid Douban subject ID');
18
+ });
19
+
20
+ it('promotes thumbnail urls to large photo urls', () => {
21
+ expect(
22
+ promoteDoubanPhotoUrl('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp'),
23
+ ).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp');
24
+
25
+ expect(
26
+ promoteDoubanPhotoUrl('https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2578474613.jpg'),
27
+ ).toBe('https://img9.doubanio.com/view/photo/l/public/p2578474613.jpg');
28
+ });
29
+
30
+ it('rejects non-http photo urls during promotion', () => {
31
+ expect(promoteDoubanPhotoUrl('data:image/gif;base64,abc')).toBe('');
32
+ });
33
+
34
+ it('prefers lazy-loaded photo urls over data placeholders', () => {
35
+ expect(
36
+ resolveDoubanPhotoAssetUrl([
37
+ '',
38
+ 'https://img1.doubanio.com/view/photo/m/public/p2913450214.webp',
39
+ 'data:image/gif;base64,abc',
40
+ ], 'https://movie.douban.com/subject/30382501/photos?type=Rb'),
41
+ ).toBe('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp');
42
+ });
43
+
44
+ it('drops unsupported non-http photo urls when no real image url exists', () => {
45
+ expect(
46
+ resolveDoubanPhotoAssetUrl(
47
+ ['data:image/gif;base64,abc', 'blob:https://movie.douban.com/example'],
48
+ 'https://movie.douban.com/subject/30382501/photos?type=Rb',
49
+ ),
50
+ ).toBe('');
51
+ });
52
+
53
+ it('removes the default photo cap when scanning for an exact photo id', async () => {
54
+ const evaluate = vi.fn()
55
+ .mockResolvedValueOnce({ blocked: false, title: 'Some Movie', href: 'https://movie.douban.com/subject/30382501/photos?type=Rb' })
56
+ .mockResolvedValueOnce({
57
+ subjectId: '30382501',
58
+ subjectTitle: 'The Wandering Earth 2',
59
+ type: 'Rb',
60
+ photos: [
61
+ {
62
+ index: 731,
63
+ photoId: '2913450215',
64
+ title: 'Character poster',
65
+ imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
66
+ thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450215.jpg',
67
+ detailUrl: 'https://movie.douban.com/photos/photo/2913450215/',
68
+ page: 25,
69
+ },
70
+ ],
71
+ });
72
+ const page = {
73
+ goto: vi.fn().mockResolvedValue(undefined),
74
+ wait: vi.fn().mockResolvedValue(undefined),
75
+ evaluate,
76
+ } as unknown as IPage;
77
+
78
+ await loadDoubanSubjectPhotos(page, '30382501', {
79
+ type: 'Rb',
80
+ targetPhotoId: '2913450215',
81
+ });
82
+
83
+ const scanScript = evaluate.mock.calls[1]?.[0];
84
+ expect(scanScript).toContain('const targetPhotoId = "2913450215";');
85
+ expect(scanScript).toContain(`const limit = ${Number.MAX_SAFE_INTEGER};`);
86
+ expect(scanScript).toContain('for (let pageIndex = 0; photos.length < limit; pageIndex += 1)');
87
+ });
88
+
89
+ it('keeps image extensions when download urls contain query params', () => {
90
+ expect(
91
+ getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp?foo=1'),
92
+ ).toBe('.webp');
93
+ expect(
94
+ getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.jpeg'),
95
+ ).toBe('.jpeg');
96
+ });
97
+ });
@@ -2,13 +2,20 @@
2
2
  * Douban adapter utilities.
3
3
  */
4
4
 
5
- import { CliError } from '../../errors.js';
5
+ import { ArgumentError, CliError, EmptyResultError } from '../../errors.js';
6
6
  import type { IPage } from '../../types.js';
7
7
 
8
+ const DOUBAN_PHOTO_PAGE_SIZE = 30;
9
+ const MAX_DOUBAN_PHOTOS = 500;
10
+
8
11
  function clampLimit(limit: number): number {
9
12
  return Math.max(1, Math.min(limit || 20, 50));
10
13
  }
11
14
 
15
+ function clampPhotoLimit(limit: number): number {
16
+ return Math.max(1, Math.min(limit || 120, MAX_DOUBAN_PHOTOS));
17
+ }
18
+
12
19
  async function ensureDoubanReady(page: IPage): Promise<void> {
13
20
  const state = await page.evaluate(`
14
21
  (() => {
@@ -27,6 +34,230 @@ async function ensureDoubanReady(page: IPage): Promise<void> {
27
34
  }
28
35
  }
29
36
 
37
+ export interface DoubanSubjectPhoto {
38
+ index: number;
39
+ photoId: string;
40
+ title: string;
41
+ imageUrl: string;
42
+ thumbUrl: string;
43
+ detailUrl: string;
44
+ page: number;
45
+ }
46
+
47
+ export interface DoubanSubjectPhotosResult {
48
+ subjectId: string;
49
+ subjectTitle: string;
50
+ type: string;
51
+ photos: DoubanSubjectPhoto[];
52
+ }
53
+
54
+ export interface LoadDoubanSubjectPhotosOptions {
55
+ type?: string;
56
+ limit?: number;
57
+ targetPhotoId?: string;
58
+ }
59
+
60
+ export function normalizeDoubanSubjectId(subjectId: string): string {
61
+ const normalized = String(subjectId || '').trim();
62
+ if (!/^\d+$/.test(normalized)) {
63
+ throw new ArgumentError(`Invalid Douban subject ID: ${subjectId}`);
64
+ }
65
+ return normalized;
66
+ }
67
+
68
+ export function promoteDoubanPhotoUrl(url: string, size: 's' | 'm' | 'l' = 'l'): string {
69
+ const normalized = String(url || '').trim();
70
+ if (!normalized) return '';
71
+ if (/^[a-z]+:/i.test(normalized) && !/^https?:/i.test(normalized)) return '';
72
+ return normalized.replace(/\/view\/photo\/[^/]+\/public\//, `/view/photo/${size}/public/`);
73
+ }
74
+
75
+ export function resolveDoubanPhotoAssetUrl(
76
+ candidates: Array<string | null | undefined>,
77
+ baseUrl = '',
78
+ ): string {
79
+ for (const candidate of candidates) {
80
+ const normalized = String(candidate || '').trim();
81
+ if (!normalized) continue;
82
+
83
+ let resolved = normalized;
84
+ try {
85
+ resolved = baseUrl
86
+ ? new URL(normalized, baseUrl).toString()
87
+ : new URL(normalized).toString();
88
+ } catch {
89
+ resolved = normalized;
90
+ }
91
+
92
+ if (/^https?:\/\//i.test(resolved)) {
93
+ return resolved;
94
+ }
95
+ }
96
+
97
+ return '';
98
+ }
99
+
100
+ export function getDoubanPhotoExtension(url: string): string {
101
+ const normalized = String(url || '').trim();
102
+ if (!normalized) return '.jpg';
103
+
104
+ try {
105
+ const ext = new URL(normalized).pathname.match(/\.(jpe?g|png|gif|webp|avif|bmp)$/i)?.[0];
106
+ return ext || '.jpg';
107
+ } catch {
108
+ const ext = normalized.match(/\.(jpe?g|png|gif|webp|avif|bmp)(?:$|[?#])/i)?.[0];
109
+ return ext ? ext.replace(/[?#].*$/, '') : '.jpg';
110
+ }
111
+ }
112
+
113
+ export async function loadDoubanSubjectPhotos(
114
+ page: IPage,
115
+ subjectId: string,
116
+ options: LoadDoubanSubjectPhotosOptions = {},
117
+ ): Promise<DoubanSubjectPhotosResult> {
118
+ const normalizedId = normalizeDoubanSubjectId(subjectId);
119
+ const type = String(options.type || 'Rb').trim() || 'Rb';
120
+ const targetPhotoId = String(options.targetPhotoId || '').trim();
121
+ const safeLimit = targetPhotoId ? Number.MAX_SAFE_INTEGER : clampPhotoLimit(Number(options.limit) || 120);
122
+ const resolvePhotoAssetUrlSource = resolveDoubanPhotoAssetUrl.toString();
123
+
124
+ const galleryUrl = `https://movie.douban.com/subject/${normalizedId}/photos?type=${encodeURIComponent(type)}`;
125
+ await page.goto(galleryUrl);
126
+ await page.wait(2);
127
+ await ensureDoubanReady(page);
128
+
129
+ const data = await page.evaluate(`
130
+ (async () => {
131
+ const subjectId = ${JSON.stringify(normalizedId)};
132
+ const type = ${JSON.stringify(type)};
133
+ const limit = ${safeLimit};
134
+ const targetPhotoId = ${JSON.stringify(targetPhotoId)};
135
+ const pageSize = ${DOUBAN_PHOTO_PAGE_SIZE};
136
+ const resolveDoubanPhotoAssetUrl = ${resolvePhotoAssetUrlSource};
137
+
138
+ const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
139
+ const toAbsoluteUrl = (value) => {
140
+ if (!value) return '';
141
+ try {
142
+ return new URL(value, location.origin).toString();
143
+ } catch {
144
+ return value;
145
+ }
146
+ };
147
+ const promotePhotoUrl = (value) => {
148
+ const absolute = toAbsoluteUrl(value);
149
+ if (!absolute) return '';
150
+ if (/^[a-z]+:/i.test(absolute) && !/^https?:/i.test(absolute)) return '';
151
+ return absolute.replace(/\\/view\\/photo\\/[^/]+\\/public\\//, '/view/photo/l/public/');
152
+ };
153
+ const buildPageUrl = (start) => {
154
+ const url = new URL(location.href);
155
+ url.searchParams.set('type', type);
156
+ if (start > 0) url.searchParams.set('start', String(start));
157
+ else url.searchParams.delete('start');
158
+ return url.toString();
159
+ };
160
+ const getTitle = (doc) => {
161
+ const raw = normalize(doc.querySelector('#content h1')?.textContent)
162
+ || normalize(doc.querySelector('title')?.textContent);
163
+ return raw.replace(/\\s*\\(豆瓣\\)\\s*$/, '');
164
+ };
165
+ const extractPhotos = (doc, pageNumber) => {
166
+ const nodes = Array.from(doc.querySelectorAll('.poster-col3 li, .poster-col3l li, .article li'));
167
+ const rows = [];
168
+ for (const node of nodes) {
169
+ const link = node.querySelector('a[href*="/photos/photo/"]');
170
+ const img = node.querySelector('img');
171
+ if (!link || !img) continue;
172
+
173
+ const detailUrl = toAbsoluteUrl(link.getAttribute('href') || '');
174
+ const photoId = detailUrl.match(/\\/photo\\/(\\d+)/)?.[1] || '';
175
+ const thumbUrl = resolveDoubanPhotoAssetUrl([
176
+ img.getAttribute('data-origin'),
177
+ img.getAttribute('data-src'),
178
+ img.getAttribute('src'),
179
+ ], location.href);
180
+ const imageUrl = promotePhotoUrl(thumbUrl);
181
+ const title = normalize(link.getAttribute('title'))
182
+ || normalize(img.getAttribute('alt'))
183
+ || (photoId ? 'photo_' + photoId : 'photo_' + String(rows.length + 1));
184
+
185
+ if (!detailUrl || !thumbUrl || !imageUrl) continue;
186
+
187
+ rows.push({
188
+ photoId,
189
+ title,
190
+ imageUrl,
191
+ thumbUrl,
192
+ detailUrl,
193
+ page: pageNumber,
194
+ });
195
+ }
196
+ return rows;
197
+ };
198
+
199
+ const subjectTitle = getTitle(document);
200
+ const seen = new Set();
201
+ const photos = [];
202
+
203
+ for (let pageIndex = 0; photos.length < limit; pageIndex += 1) {
204
+ let doc = document;
205
+ if (pageIndex > 0) {
206
+ const response = await fetch(buildPageUrl(pageIndex * pageSize), { credentials: 'include' });
207
+ if (!response.ok) break;
208
+ const html = await response.text();
209
+ doc = new DOMParser().parseFromString(html, 'text/html');
210
+ }
211
+
212
+ const pagePhotos = extractPhotos(doc, pageIndex + 1);
213
+ if (!pagePhotos.length) break;
214
+
215
+ let appended = 0;
216
+ let foundTarget = false;
217
+ for (const photo of pagePhotos) {
218
+ const key = photo.photoId || photo.detailUrl || photo.imageUrl;
219
+ if (seen.has(key)) continue;
220
+ seen.add(key);
221
+ photos.push({
222
+ index: photos.length + 1,
223
+ ...photo,
224
+ });
225
+ appended += 1;
226
+ if (targetPhotoId && photo.photoId === targetPhotoId) {
227
+ foundTarget = true;
228
+ break;
229
+ }
230
+ if (photos.length >= limit) break;
231
+ }
232
+
233
+ if (foundTarget || pagePhotos.length < pageSize || appended === 0) break;
234
+ }
235
+
236
+ return {
237
+ subjectId,
238
+ subjectTitle,
239
+ type,
240
+ photos,
241
+ };
242
+ })()
243
+ `);
244
+
245
+ const photos = Array.isArray(data?.photos) ? data.photos : [];
246
+ if (!photos.length) {
247
+ throw new EmptyResultError(
248
+ 'douban photos',
249
+ 'No photos found. Try a different subject ID or a different --type value such as Rb.',
250
+ );
251
+ }
252
+
253
+ return {
254
+ subjectId: normalizedId,
255
+ subjectTitle: String(data?.subjectTitle || '').trim(),
256
+ type,
257
+ photos,
258
+ };
259
+ }
260
+
30
261
  export async function loadDoubanBookHot(page: IPage, limit: number): Promise<any[]> {
31
262
  const safeLimit = clampLimit(limit);
32
263
  await page.goto('https://book.douban.com/chart');
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { IPage } from '../../../types.js';
3
+ import { browserFetch } from './browser-fetch.js';
4
+
5
+ function makePage(result: unknown): IPage {
6
+ return {
7
+ goto: vi.fn(), evaluate: vi.fn().mockResolvedValue(result),
8
+ getCookies: vi.fn(), snapshot: vi.fn(), click: vi.fn(),
9
+ typeText: vi.fn(), pressKey: vi.fn(), scrollTo: vi.fn(),
10
+ getFormState: vi.fn(), wait: vi.fn(), tabs: vi.fn(),
11
+ closeTab: vi.fn(), newTab: vi.fn(), selectTab: vi.fn(),
12
+ networkRequests: vi.fn(), consoleMessages: vi.fn(),
13
+ scroll: vi.fn(), autoScroll: vi.fn(),
14
+ installInterceptor: vi.fn(), getInterceptedRequests: vi.fn(),
15
+ screenshot: vi.fn(),
16
+ } as unknown as IPage;
17
+ }
18
+
19
+ describe('browserFetch', () => {
20
+ it('returns parsed JSON on success', async () => {
21
+ const page = makePage({ status_code: 0, data: { ak: 'KEY' } });
22
+ const result = await browserFetch(page, 'GET', 'https://creator.douyin.com/api/test');
23
+ expect(result).toEqual({ status_code: 0, data: { ak: 'KEY' } });
24
+ });
25
+
26
+ it('throws when status_code is non-zero', async () => {
27
+ const page = makePage({ status_code: 8, message: 'fail' });
28
+ await expect(
29
+ browserFetch(page, 'GET', 'https://creator.douyin.com/api/test')
30
+ ).rejects.toThrow('Douyin API error 8');
31
+ });
32
+
33
+ it('returns result even when no status_code field', async () => {
34
+ const page = makePage({ some_field: 'value' });
35
+ const result = await browserFetch(page, 'GET', 'https://creator.douyin.com/api/test');
36
+ expect(result).toEqual({ some_field: 'value' });
37
+ });
38
+ });
@@ -0,0 +1,45 @@
1
+ import type { IPage } from '../../../types.js';
2
+ import { CommandExecutionError } from '../../../errors.js';
3
+
4
+ export interface FetchOptions {
5
+ body?: unknown;
6
+ headers?: Record<string, string>;
7
+ }
8
+
9
+ /**
10
+ * Execute a fetch() call inside the Chrome browser context via page.evaluate.
11
+ * This ensures a_bogus signing and cookies are handled automatically by the browser.
12
+ */
13
+ export async function browserFetch(
14
+ page: IPage,
15
+ method: 'GET' | 'POST',
16
+ url: string,
17
+ options: FetchOptions = {}
18
+ ): Promise<unknown> {
19
+ const js = `
20
+ (async () => {
21
+ const res = await fetch(${JSON.stringify(url)}, {
22
+ method: ${JSON.stringify(method)},
23
+ credentials: 'include',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ ...${JSON.stringify(options.headers ?? {})}
27
+ },
28
+ ${options.body ? `body: JSON.stringify(${JSON.stringify(options.body)}),` : ''}
29
+ });
30
+ return res.json();
31
+ })()
32
+ `;
33
+
34
+ const result = await page.evaluate(js);
35
+
36
+ if (result && typeof result === 'object' && 'status_code' in result) {
37
+ const code = (result as { status_code: number }).status_code;
38
+ if (code !== 0) {
39
+ const msg = (result as { status_msg?: string }).status_msg ?? 'unknown error';
40
+ throw new CommandExecutionError(`Douyin API error ${code}: ${msg}`);
41
+ }
42
+ }
43
+
44
+ return result;
45
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { generateCreationId } from './creation-id.js';
3
+
4
+ describe('generateCreationId', () => {
5
+ it('starts with "pin"', () => {
6
+ expect(generateCreationId()).toMatch(/^pin/);
7
+ });
8
+
9
+ it('has 4 random lowercase-alphanumeric chars after "pin"', () => {
10
+ expect(generateCreationId()).toMatch(/^pin[a-z0-9]{4}/);
11
+ });
12
+
13
+ it('ends with a numeric timestamp (ms)', () => {
14
+ const before = Date.now();
15
+ const id = generateCreationId();
16
+ const after = Date.now();
17
+ const ts = parseInt(id.replace(/^pin[a-z0-9]{4}/, ''), 10);
18
+ expect(ts).toBeGreaterThanOrEqual(before);
19
+ expect(ts).toBeLessThanOrEqual(after);
20
+ });
21
+
22
+ it('generates unique IDs', () => {
23
+ const ids = new Set(Array.from({ length: 100 }, generateCreationId));
24
+ expect(ids.size).toBe(100);
25
+ });
26
+ });
@@ -0,0 +1,8 @@
1
+ const CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789';
2
+
3
+ export function generateCreationId(): string {
4
+ const random = Array.from({ length: 4 }, () =>
5
+ CHARS[Math.floor(Math.random() * CHARS.length)]
6
+ ).join('');
7
+ return 'pin' + random + Date.now();
8
+ }
@@ -0,0 +1,113 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { CommandExecutionError } from '../../../errors.js';
6
+ import { imagexUpload } from './imagex-upload.js';
7
+ import type { ImageXUploadInfo } from './imagex-upload.js';
8
+
9
+ // ── Helpers ──────────────────────────────────────────────────────────────────
10
+
11
+ function makeTempImage(ext = '.jpg'): string {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'imagex-test-'));
13
+ const filePath = path.join(dir, `cover${ext}`);
14
+ fs.writeFileSync(filePath, Buffer.from([0xff, 0xd8, 0xff, 0xe0])); // minimal JPEG header bytes
15
+ return filePath;
16
+ }
17
+
18
+ const FAKE_UPLOAD_INFO: ImageXUploadInfo = {
19
+ upload_url: 'https://imagex.bytedance.com/upload/presigned/fake',
20
+ store_uri: 'tos-cn-i-alisg.example.com/cover/abc123',
21
+ };
22
+
23
+ // ── Tests ─────────────────────────────────────────────────────────────────────
24
+
25
+ describe('imagexUpload', () => {
26
+ let imagePath: string;
27
+
28
+ beforeEach(() => {
29
+ imagePath = makeTempImage('.jpg');
30
+ });
31
+
32
+ afterEach(() => {
33
+ // Clean up temp files
34
+ try {
35
+ fs.unlinkSync(imagePath);
36
+ fs.rmdirSync(path.dirname(imagePath));
37
+ } catch {
38
+ // ignore cleanup errors
39
+ }
40
+ vi.restoreAllMocks();
41
+ });
42
+
43
+ it('throws CommandExecutionError when image file does not exist', async () => {
44
+ await expect(
45
+ imagexUpload('/nonexistent/path/cover.jpg', FAKE_UPLOAD_INFO),
46
+ ).rejects.toThrow(CommandExecutionError);
47
+
48
+ await expect(
49
+ imagexUpload('/nonexistent/path/cover.jpg', FAKE_UPLOAD_INFO),
50
+ ).rejects.toThrow('Cover image file not found');
51
+ });
52
+
53
+ it('PUTs the image and returns store_uri on success', async () => {
54
+ const mockFetch = vi.fn().mockResolvedValue({
55
+ ok: true,
56
+ status: 200,
57
+ text: vi.fn().mockResolvedValue(''),
58
+ });
59
+ vi.stubGlobal('fetch', mockFetch);
60
+
61
+ const result = await imagexUpload(imagePath, FAKE_UPLOAD_INFO);
62
+
63
+ expect(result).toBe(FAKE_UPLOAD_INFO.store_uri);
64
+ expect(mockFetch).toHaveBeenCalledOnce();
65
+ const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
66
+ expect(url).toBe(FAKE_UPLOAD_INFO.upload_url);
67
+ expect(init.method).toBe('PUT');
68
+ expect((init.headers as Record<string, string>)['Content-Type']).toBe(
69
+ 'image/jpeg',
70
+ );
71
+ });
72
+
73
+ it('uses image/png Content-Type for .png files', async () => {
74
+ const pngPath = makeTempImage('.png');
75
+ const mockFetch = vi.fn().mockResolvedValue({
76
+ ok: true,
77
+ status: 200,
78
+ text: vi.fn().mockResolvedValue(''),
79
+ });
80
+ vi.stubGlobal('fetch', mockFetch);
81
+
82
+ try {
83
+ await imagexUpload(pngPath, FAKE_UPLOAD_INFO);
84
+ const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
85
+ expect((init.headers as Record<string, string>)['Content-Type']).toBe(
86
+ 'image/png',
87
+ );
88
+ } finally {
89
+ try {
90
+ fs.unlinkSync(pngPath);
91
+ fs.rmdirSync(path.dirname(pngPath));
92
+ } catch {
93
+ // ignore
94
+ }
95
+ }
96
+ });
97
+
98
+ it('throws CommandExecutionError on non-2xx PUT response', async () => {
99
+ const mockFetch = vi.fn().mockResolvedValue({
100
+ ok: false,
101
+ status: 403,
102
+ text: vi.fn().mockResolvedValue('Forbidden'),
103
+ });
104
+ vi.stubGlobal('fetch', mockFetch);
105
+
106
+ await expect(imagexUpload(imagePath, FAKE_UPLOAD_INFO)).rejects.toThrow(
107
+ CommandExecutionError,
108
+ );
109
+ await expect(imagexUpload(imagePath, FAKE_UPLOAD_INFO)).rejects.toThrow(
110
+ 'ImageX upload failed with status 403',
111
+ );
112
+ });
113
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ImageX cover image uploader.
3
+ *
4
+ * Uploads a JPEG/PNG image to ByteDance ImageX via a pre-signed PUT URL
5
+ * obtained from the Douyin "apply cover upload" API.
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { CommandExecutionError } from '../../../errors.js';
11
+
12
+ export interface ImageXUploadInfo {
13
+ /** Pre-signed PUT target URL (provided by the apply cover upload API) */
14
+ upload_url: string;
15
+ /** Image URI to use in create_v2 (returned from the apply step) */
16
+ store_uri: string;
17
+ }
18
+
19
+ /**
20
+ * Detect MIME type from file extension.
21
+ * Falls back to image/jpeg for unknown extensions.
22
+ */
23
+ function detectContentType(filePath: string): string {
24
+ const ext = path.extname(filePath).toLowerCase();
25
+ switch (ext) {
26
+ case '.png':
27
+ return 'image/png';
28
+ case '.gif':
29
+ return 'image/gif';
30
+ case '.webp':
31
+ return 'image/webp';
32
+ default:
33
+ return 'image/jpeg';
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Upload a cover image to ByteDance ImageX via a pre-signed PUT URL.
39
+ *
40
+ * @param imagePath - Local file path to the image (JPEG/PNG/etc.)
41
+ * @param uploadInfo - Upload URL and store_uri from the apply cover upload API
42
+ * @returns The store_uri (= image_uri for use in create_v2)
43
+ */
44
+ export async function imagexUpload(
45
+ imagePath: string,
46
+ uploadInfo: ImageXUploadInfo,
47
+ ): Promise<string> {
48
+ if (!fs.existsSync(imagePath)) {
49
+ throw new CommandExecutionError(
50
+ `Cover image file not found: ${imagePath}`,
51
+ 'Ensure the file path is correct and accessible.',
52
+ );
53
+ }
54
+
55
+ const imageBuffer = fs.readFileSync(imagePath);
56
+ const contentType = detectContentType(imagePath);
57
+
58
+ const res = await fetch(uploadInfo.upload_url, {
59
+ method: 'PUT',
60
+ headers: {
61
+ 'Content-Type': contentType,
62
+ 'Content-Length': String(imageBuffer.byteLength),
63
+ },
64
+ body: imageBuffer as unknown as BodyInit,
65
+ });
66
+
67
+ if (!res.ok) {
68
+ const body = await res.text().catch(() => '');
69
+ throw new CommandExecutionError(
70
+ `ImageX upload failed with status ${res.status}: ${body}`,
71
+ 'Check that the upload URL is valid and has not expired.',
72
+ );
73
+ }
74
+
75
+ return uploadInfo.store_uri;
76
+ }
@@ -0,0 +1,20 @@
1
+ import type { IPage } from '../../../types.js';
2
+ import type { Sts2Credentials } from './types.js';
3
+ import { AuthRequiredError } from '../../../errors.js';
4
+
5
+ const STS2_URL =
6
+ 'https://creator.douyin.com/aweme/mid/video/sts2/?scene=web&aid=1128&cookie_enabled=true&device_platform=web';
7
+
8
+ /**
9
+ * Fetch STS2 temporary credentials from the creator center.
10
+ * These are used to authenticate Node.js-side TOS multipart uploads.
11
+ * Returns: { access_key_id, secret_access_key, session_token, expired_time }
12
+ */
13
+ export async function getSts2Credentials(page: IPage): Promise<Sts2Credentials> {
14
+ const js = `fetch(${JSON.stringify(STS2_URL)}, { credentials: 'include' }).then(r => r.json())`;
15
+ const res = await page.evaluate(js) as { data: Sts2Credentials };
16
+ if (!res?.data?.access_key_id) {
17
+ throw new AuthRequiredError('creator.douyin.com', 'STS2 credentials missing');
18
+ }
19
+ return res.data;
20
+ }