@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
@@ -2,7 +2,7 @@ import { execSync, spawnSync } from 'node:child_process';
2
2
  import { cli, Strategy } from '../../registry.js';
3
3
  import { ConfigError } from '../../errors.js';
4
4
  import type { IPage } from '../../types.js';
5
- import { getVisibleChatMessages } from './ax.js';
5
+ import { activateChatGPT, getVisibleChatMessages, selectModel, MODEL_CHOICES, isGenerating } from './ax.js';
6
6
 
7
7
  export const askCommand = cli({
8
8
  site: 'chatgpt',
@@ -13,6 +13,7 @@ export const askCommand = cli({
13
13
  browser: false,
14
14
  args: [
15
15
  { name: 'text', required: true, positional: true, help: 'Prompt to send' },
16
+ { name: 'model', required: false, help: 'Model/mode to use: auto, instant, thinking, 5.2-instant, 5.2-thinking', choices: MODEL_CHOICES },
16
17
  { name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default: '30' },
17
18
  ],
18
19
  columns: ['Role', 'Text'],
@@ -22,8 +23,15 @@ export const askCommand = cli({
22
23
  }
23
24
 
24
25
  const text = kwargs.text as string;
26
+ const model = kwargs.model as string | undefined;
25
27
  const timeout = parseInt(kwargs.timeout as string, 10) || 30;
26
28
 
29
+ // Switch model before sending if requested
30
+ if (model) {
31
+ activateChatGPT();
32
+ selectModel(model);
33
+ }
34
+
27
35
  // Backup clipboard
28
36
  let clipBackup = '';
29
37
  try { clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); } catch {}
@@ -31,8 +39,7 @@ export const askCommand = cli({
31
39
 
32
40
  // Send the message
33
41
  spawnSync('pbcopy', { input: text });
34
- execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
35
- execSync("osascript -e 'delay 0.5'");
42
+ activateChatGPT();
36
43
 
37
44
  const cmd = "osascript " +
38
45
  "-e 'tell application \"System Events\"' " +
@@ -45,25 +52,32 @@ export const askCommand = cli({
45
52
  // Restore clipboard after the prompt is sent.
46
53
  if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
47
54
 
48
- // Wait for response, then read the latest visible assistant message from the AX tree.
49
- const pollInterval = 1;
55
+ // Wait for response: poll until ChatGPT stops generating ("Stop generating" button disappears),
56
+ // then read the final response text.
57
+ const pollInterval = 2;
50
58
  const maxPolls = Math.ceil(timeout / pollInterval);
51
59
  let response = '';
60
+ let generationStarted = false;
52
61
 
53
62
  for (let i = 0; i < maxPolls; i++) {
54
63
  execSync(`sleep ${pollInterval}`);
55
- execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
56
- execSync("osascript -e 'delay 0.2'");
64
+ const generating = isGenerating();
65
+ if (generating) {
66
+ generationStarted = true;
67
+ continue;
68
+ }
69
+ // Generation finished (or never started yet)
70
+ if (!generationStarted && i < 3) continue; // give it a moment to start
57
71
 
72
+ // Read final response
73
+ activateChatGPT(0.3);
58
74
  const messagesNow = getVisibleChatMessages();
59
- if (messagesNow.length <= messagesBefore.length) continue;
60
-
61
- const newMessages = messagesNow.slice(messagesBefore.length);
62
- const candidate = [...newMessages].reverse().find((message) => message !== text);
63
- if (candidate) {
64
- response = candidate;
65
- break;
75
+ if (messagesNow.length > messagesBefore.length) {
76
+ const newMessages = messagesNow.slice(messagesBefore.length);
77
+ const candidate = [...newMessages].reverse().find((message) => message !== text);
78
+ if (candidate) response = candidate;
66
79
  }
80
+ break;
67
81
  }
68
82
 
69
83
  if (!response) {
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { execFileSync, execSync } from 'node:child_process';
2
2
 
3
3
  const AX_READ_SCRIPT = `
4
4
  import Cocoa
@@ -62,6 +62,185 @@ let data = try! JSONSerialization.data(withJSONObject: best, options: [])
62
62
  print(String(data: data, encoding: .utf8)!)
63
63
  `;
64
64
 
65
+ const AX_MODEL_SCRIPT = `
66
+ import Cocoa
67
+ import ApplicationServices
68
+
69
+ func attr(_ el: AXUIElement, _ name: String) -> AnyObject? {
70
+ var value: CFTypeRef?
71
+ guard AXUIElementCopyAttributeValue(el, name as CFString, &value) == .success else { return nil }
72
+ return value as AnyObject?
73
+ }
74
+
75
+ func s(_ el: AXUIElement, _ name: String) -> String? {
76
+ if let v = attr(el, name) as? String, !v.isEmpty { return v }
77
+ return nil
78
+ }
79
+
80
+ func children(_ el: AXUIElement) -> [AXUIElement] {
81
+ (attr(el, kAXChildrenAttribute as String) as? [AnyObject] ?? []).map { $0 as! AXUIElement }
82
+ }
83
+
84
+ func press(_ el: AXUIElement) {
85
+ AXUIElementPerformAction(el, kAXPressAction as CFString)
86
+ }
87
+
88
+ func findByDesc(_ el: AXUIElement, _ target: String, prefix: Bool = false, depth: Int = 0) -> AXUIElement? {
89
+ guard depth < 20 else { return nil }
90
+ let desc = s(el, kAXDescriptionAttribute as String) ?? ""
91
+ if prefix ? desc.hasPrefix(target) : (desc == target) { return el }
92
+ for c in children(el) {
93
+ if let found = findByDesc(c, target, prefix: prefix, depth: depth + 1) { return found }
94
+ }
95
+ return nil
96
+ }
97
+
98
+ func findPopover(_ el: AXUIElement, depth: Int = 0) -> AXUIElement? {
99
+ guard depth < 20 else { return nil }
100
+ let role = s(el, kAXRoleAttribute as String) ?? ""
101
+ if role == "AXPopover" { return el }
102
+ for c in children(el) {
103
+ if let found = findPopover(c, depth: depth + 1) { return found }
104
+ }
105
+ return nil
106
+ }
107
+
108
+ func pressEscape() {
109
+ let src = CGEventSource(stateID: .combinedSessionState)
110
+ if let esc = CGEvent(keyboardEventSource: src, virtualKey: 0x35, keyDown: true) { esc.post(tap: .cghidEventTap) }
111
+ if let esc = CGEvent(keyboardEventSource: src, virtualKey: 0x35, keyDown: false) { esc.post(tap: .cghidEventTap) }
112
+ }
113
+
114
+ guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.openai.chat").first else {
115
+ fputs("ChatGPT not running\\n", stderr); exit(1)
116
+ }
117
+ let axApp = AXUIElementCreateApplication(app.processIdentifier)
118
+ guard let win = attr(axApp, kAXFocusedWindowAttribute as String) as! AXUIElement? else {
119
+ fputs("No focused ChatGPT window\\n", stderr); exit(1)
120
+ }
121
+
122
+ let args = CommandLine.arguments
123
+ let target = args.count > 1 ? args[1] : ""
124
+ let needsLegacy = args.count > 2 && args[2] == "legacy"
125
+
126
+ // Step 1: Click the "Options" button to open the popover
127
+ guard let optionsBtn = findByDesc(win, "Options") else {
128
+ fputs("Could not find Options button\\n", stderr); exit(1)
129
+ }
130
+ press(optionsBtn)
131
+ Thread.sleep(forTimeInterval: 0.8)
132
+
133
+ // Step 2: Find the popover that appeared, search ONLY within it
134
+ guard let popover = findPopover(win) else {
135
+ pressEscape()
136
+ fputs("Popover did not appear\\n", stderr); exit(1)
137
+ }
138
+
139
+ // Step 3: If legacy, click "Legacy models" to expand submenu
140
+ if needsLegacy {
141
+ guard let legacyBtn = findByDesc(popover, "Legacy models") else {
142
+ pressEscape()
143
+ fputs("Could not find Legacy models button\\n", stderr); exit(1)
144
+ }
145
+ press(legacyBtn)
146
+ Thread.sleep(forTimeInterval: 0.8)
147
+ }
148
+
149
+ // Step 4: Click the target model button within the popover (prefix match)
150
+ guard let modelBtn = findByDesc(popover, target, prefix: true) else {
151
+ pressEscape()
152
+ fputs("Could not find button starting with '\\(target)' in popover\\n", stderr); exit(1)
153
+ }
154
+ press(modelBtn)
155
+ print("Selected: \\(target)")
156
+ `;
157
+
158
+ const AX_GENERATING_SCRIPT = `
159
+ import Cocoa
160
+ import ApplicationServices
161
+
162
+ func attr(_ el: AXUIElement, _ name: String) -> AnyObject? {
163
+ var value: CFTypeRef?
164
+ guard AXUIElementCopyAttributeValue(el, name as CFString, &value) == .success else { return nil }
165
+ return value as AnyObject?
166
+ }
167
+
168
+ func s(_ el: AXUIElement, _ name: String) -> String? {
169
+ if let v = attr(el, name) as? String, !v.isEmpty { return v }
170
+ return nil
171
+ }
172
+
173
+ func children(_ el: AXUIElement) -> [AXUIElement] {
174
+ (attr(el, kAXChildrenAttribute as String) as? [AnyObject] ?? []).map { $0 as! AXUIElement }
175
+ }
176
+
177
+ func hasButton(_ el: AXUIElement, desc target: String, depth: Int = 0) -> Bool {
178
+ guard depth < 15 else { return false }
179
+ let role = s(el, kAXRoleAttribute as String) ?? ""
180
+ let desc = s(el, kAXDescriptionAttribute as String) ?? ""
181
+ if role == "AXButton" && desc == target { return true }
182
+ for c in children(el) {
183
+ if hasButton(c, desc: target, depth: depth + 1) { return true }
184
+ }
185
+ return false
186
+ }
187
+
188
+ guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.openai.chat").first else {
189
+ print("false"); exit(0)
190
+ }
191
+ let axApp = AXUIElementCreateApplication(app.processIdentifier)
192
+ guard let win = attr(axApp, kAXFocusedWindowAttribute as String) as! AXUIElement? else {
193
+ print("false"); exit(0)
194
+ }
195
+ print(hasButton(win, desc: "Stop generating") ? "true" : "false")
196
+ `;
197
+
198
+ type ModelChoice = 'auto' | 'instant' | 'thinking' | '5.2-instant' | '5.2-thinking';
199
+
200
+ const MODEL_MAP: Record<ModelChoice, { desc: string; legacy?: boolean }> = {
201
+ 'auto': { desc: 'Auto' },
202
+ 'instant': { desc: 'Instant' },
203
+ 'thinking': { desc: 'Thinking' },
204
+ '5.2-instant': { desc: 'GPT-5.2 Instant', legacy: true },
205
+ '5.2-thinking': { desc: 'GPT-5.2 Thinking', legacy: true },
206
+ };
207
+
208
+ export const MODEL_CHOICES = Object.keys(MODEL_MAP) as ModelChoice[];
209
+
210
+ export function activateChatGPT(delaySeconds: number = 0.5): void {
211
+ execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
212
+ execSync(`osascript -e 'delay ${delaySeconds}'`);
213
+ }
214
+
215
+ export function selectModel(model: string): string {
216
+ const entry = MODEL_MAP[model as ModelChoice];
217
+ if (!entry) {
218
+ throw new Error(`Unknown model "${model}". Choose from: ${MODEL_CHOICES.join(', ')}`);
219
+ }
220
+ const swiftArgs = ['-', entry.desc];
221
+ if (entry.legacy) swiftArgs.push('legacy');
222
+
223
+ const output = execFileSync('swift', swiftArgs, {
224
+ input: AX_MODEL_SCRIPT,
225
+ encoding: 'utf-8',
226
+ maxBuffer: 10 * 1024 * 1024,
227
+ }).trim();
228
+ return output;
229
+ }
230
+
231
+ export function isGenerating(): boolean {
232
+ try {
233
+ const output = execFileSync('swift', ['-'], {
234
+ input: AX_GENERATING_SCRIPT,
235
+ encoding: 'utf-8',
236
+ maxBuffer: 10 * 1024 * 1024,
237
+ }).trim();
238
+ return output === 'true';
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+
65
244
  export function getVisibleChatMessages(): string[] {
66
245
  const output = execFileSync('swift', ['-'], {
67
246
  input: AX_READ_SCRIPT,
@@ -0,0 +1,27 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { ConfigError } from '../../errors.js';
3
+ import type { IPage } from '../../types.js';
4
+ import { activateChatGPT, selectModel, MODEL_CHOICES } from './ax.js';
5
+
6
+ export const modelCommand = cli({
7
+ site: 'chatgpt',
8
+ name: 'model',
9
+ description: 'Switch ChatGPT Desktop model/mode (auto, instant, thinking, 5.2-instant, 5.2-thinking)',
10
+ domain: 'localhost',
11
+ strategy: Strategy.PUBLIC,
12
+ browser: false,
13
+ args: [
14
+ { name: 'model', required: true, positional: true, help: 'Model to switch to', choices: MODEL_CHOICES },
15
+ ],
16
+ columns: ['Status', 'Model'],
17
+ func: async (page: IPage | null, kwargs: any) => {
18
+ if (process.platform !== 'darwin') {
19
+ throw new ConfigError('ChatGPT Desktop integration requires macOS');
20
+ }
21
+
22
+ const model = kwargs.model as string;
23
+ activateChatGPT();
24
+ const result = selectModel(model);
25
+ return [{ Status: 'Success', Model: result }];
26
+ },
27
+ });
@@ -2,6 +2,7 @@ import { execSync, spawnSync } from 'node:child_process';
2
2
  import { cli, Strategy } from '../../registry.js';
3
3
  import type { IPage } from '../../types.js';
4
4
  import { getErrorMessage } from '../../errors.js';
5
+ import { activateChatGPT, selectModel, MODEL_CHOICES } from './ax.js';
5
6
 
6
7
  export const sendCommand = cli({
7
8
  site: 'chatgpt',
@@ -10,11 +11,21 @@ export const sendCommand = cli({
10
11
  domain: 'localhost',
11
12
  strategy: Strategy.PUBLIC,
12
13
  browser: false,
13
- args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
14
+ args: [
15
+ { name: 'text', required: true, positional: true, help: 'Message to send' },
16
+ { name: 'model', required: false, help: 'Model/mode to use: auto, instant, thinking, 5.2-instant, 5.2-thinking', choices: MODEL_CHOICES },
17
+ ],
14
18
  columns: ['Status'],
15
19
  func: async (page: IPage | null, kwargs: any) => {
16
20
  const text = kwargs.text as string;
21
+ const model = kwargs.model as string | undefined;
17
22
  try {
23
+ // Switch model before sending if requested
24
+ if (model) {
25
+ activateChatGPT();
26
+ selectModel(model);
27
+ }
28
+
18
29
  // Backup current clipboard content
19
30
  let clipBackup = '';
20
31
  try {
@@ -23,17 +34,16 @@ export const sendCommand = cli({
23
34
 
24
35
  // Copy text to clipboard
25
36
  spawnSync('pbcopy', { input: text });
26
-
27
- execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
28
- execSync("osascript -e 'delay 0.5'");
29
-
37
+
38
+ activateChatGPT();
39
+
30
40
  const cmd = "osascript " +
31
41
  "-e 'tell application \"System Events\"' " +
32
42
  "-e 'keystroke \"v\" using command down' " +
33
43
  "-e 'delay 0.2' " +
34
44
  "-e 'keystroke return' " +
35
45
  "-e 'end tell'";
36
-
46
+
37
47
  execSync(cmd);
38
48
 
39
49
  // Restore original clipboard content
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * 携程旅行搜索 — browser cookie, multi-strategy.
3
- * Source: bb-sites/ctrip/search.js (simplified to suggestion API)
4
3
  */
5
4
  import { cli, Strategy } from '../../registry.js';
6
5
 
@@ -0,0 +1,196 @@
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import path from 'node:path';
3
+ import type { CliCommand } from '../../registry.js';
4
+ import { getRegistry } from '../../registry.js';
5
+ import type { IPage } from '../../types.js';
6
+
7
+ const { mockHttpDownload, mockLoadDoubanSubjectPhotos, mockMkdirSync } = vi.hoisted(() => ({
8
+ mockHttpDownload: vi.fn(),
9
+ mockLoadDoubanSubjectPhotos: vi.fn(),
10
+ mockMkdirSync: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('../../download/index.js', () => ({
14
+ httpDownload: mockHttpDownload,
15
+ sanitizeFilename: vi.fn((value: string) => value.replace(/\s+/g, '_')),
16
+ }));
17
+
18
+ vi.mock('./utils.js', async () => {
19
+ const actual = await vi.importActual<typeof import('./utils.js')>('./utils.js');
20
+ return {
21
+ ...actual,
22
+ loadDoubanSubjectPhotos: mockLoadDoubanSubjectPhotos,
23
+ };
24
+ });
25
+
26
+ vi.mock('../../download/progress.js', () => ({
27
+ formatBytes: vi.fn((size: number) => `${size} B`),
28
+ }));
29
+
30
+ vi.mock('node:fs', () => ({
31
+ mkdirSync: mockMkdirSync,
32
+ }));
33
+
34
+ await import('./download.js');
35
+
36
+ let cmd: CliCommand;
37
+
38
+ beforeAll(() => {
39
+ cmd = getRegistry().get('douban/download')!;
40
+ expect(cmd?.func).toBeTypeOf('function');
41
+ });
42
+
43
+ function toPosixPath(value: string): string {
44
+ return value.replaceAll(path.sep, '/');
45
+ }
46
+
47
+ describe('douban download', () => {
48
+ beforeEach(() => {
49
+ mockHttpDownload.mockReset();
50
+ mockLoadDoubanSubjectPhotos.mockReset();
51
+ mockMkdirSync.mockReset();
52
+ });
53
+
54
+ it('downloads douban poster images and merges metadata into the result', async () => {
55
+ const page = {} as IPage;
56
+ mockLoadDoubanSubjectPhotos.mockResolvedValue({
57
+ subjectId: '30382501',
58
+ subjectTitle: 'The Wandering Earth 2',
59
+ type: 'Rb',
60
+ photos: [
61
+ {
62
+ index: 1,
63
+ photoId: '2913450214',
64
+ title: 'Main poster',
65
+ imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450214.webp',
66
+ thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450214.webp',
67
+ detailUrl: 'https://movie.douban.com/photos/photo/2913450214/',
68
+ page: 1,
69
+ },
70
+ {
71
+ index: 2,
72
+ photoId: '2913450215',
73
+ title: 'Character poster',
74
+ imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
75
+ thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450215.jpg',
76
+ detailUrl: 'https://movie.douban.com/photos/photo/2913450215/',
77
+ page: 1,
78
+ },
79
+ ],
80
+ });
81
+
82
+ mockHttpDownload
83
+ .mockResolvedValueOnce({ success: true, size: 1200 })
84
+ .mockResolvedValueOnce({ success: true, size: 980 });
85
+
86
+ const result = await cmd.func!(page, {
87
+ id: '30382501',
88
+ type: 'Rb',
89
+ limit: 20,
90
+ output: '/tmp/douban-test',
91
+ }) as Array<Record<string, unknown>>;
92
+
93
+ expect(mockLoadDoubanSubjectPhotos).toHaveBeenCalledWith(page, '30382501', {
94
+ type: 'Rb',
95
+ limit: 20,
96
+ });
97
+ expect(mockMkdirSync).toHaveBeenCalledTimes(1);
98
+ expect(toPosixPath(mockMkdirSync.mock.calls[0][0])).toBe('/tmp/douban-test/30382501');
99
+ expect(mockMkdirSync.mock.calls[0][1]).toEqual({ recursive: true });
100
+ expect(mockHttpDownload).toHaveBeenCalledTimes(2);
101
+ expect(mockHttpDownload.mock.calls[0]?.[0]).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp');
102
+ expect(toPosixPath(mockHttpDownload.mock.calls[0]?.[1])).toBe('/tmp/douban-test/30382501/30382501_001_2913450214_Main_poster.webp');
103
+ expect(mockHttpDownload.mock.calls[0]?.[2]).toEqual(expect.objectContaining({
104
+ headers: { Referer: 'https://movie.douban.com/photos/photo/2913450214/' },
105
+ timeout: 60000,
106
+ }));
107
+ expect(mockHttpDownload.mock.calls[1]?.[0]).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg');
108
+ expect(toPosixPath(mockHttpDownload.mock.calls[1]?.[1])).toBe('/tmp/douban-test/30382501/30382501_002_2913450215_Character_poster.jpg');
109
+ expect(mockHttpDownload.mock.calls[1]?.[2]).toEqual(expect.objectContaining({
110
+ headers: { Referer: 'https://movie.douban.com/photos/photo/2913450215/' },
111
+ timeout: 60000,
112
+ }));
113
+
114
+ expect(result).toEqual([
115
+ {
116
+ index: 1,
117
+ title: 'Main poster',
118
+ photo_id: '2913450214',
119
+ image_url: 'https://img1.doubanio.com/view/photo/l/public/p2913450214.webp',
120
+ detail_url: 'https://movie.douban.com/photos/photo/2913450214/',
121
+ status: 'success',
122
+ size: '1200 B',
123
+ },
124
+ {
125
+ index: 2,
126
+ title: 'Character poster',
127
+ photo_id: '2913450215',
128
+ image_url: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
129
+ detail_url: 'https://movie.douban.com/photos/photo/2913450215/',
130
+ status: 'success',
131
+ size: '980 B',
132
+ },
133
+ ]);
134
+ });
135
+
136
+ it('downloads only the requested photo when photo-id is provided', async () => {
137
+ const page = {} as IPage;
138
+ mockLoadDoubanSubjectPhotos.mockResolvedValue({
139
+ subjectId: '30382501',
140
+ subjectTitle: 'The Wandering Earth 2',
141
+ type: 'Rb',
142
+ photos: [
143
+ {
144
+ index: 2,
145
+ photoId: '2913450215',
146
+ title: 'Character poster',
147
+ imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
148
+ thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450215.jpg',
149
+ detailUrl: 'https://movie.douban.com/photos/photo/2913450215/',
150
+ page: 1,
151
+ },
152
+ ],
153
+ });
154
+
155
+ mockHttpDownload.mockResolvedValueOnce({ success: true, size: 980 });
156
+
157
+ const result = await cmd.func!(page, {
158
+ id: '30382501',
159
+ type: 'Rb',
160
+ 'photo-id': '2913450215',
161
+ output: '/tmp/douban-test',
162
+ }) as Array<Record<string, unknown>>;
163
+
164
+ expect(mockLoadDoubanSubjectPhotos).toHaveBeenCalledWith(page, '30382501', {
165
+ type: 'Rb',
166
+ targetPhotoId: '2913450215',
167
+ });
168
+ expect(mockHttpDownload).toHaveBeenCalledTimes(1);
169
+ expect(mockHttpDownload.mock.calls[0]?.[0]).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg');
170
+ expect(toPosixPath(mockHttpDownload.mock.calls[0]?.[1])).toBe('/tmp/douban-test/30382501/30382501_002_2913450215_Character_poster.jpg');
171
+ expect(mockHttpDownload.mock.calls[0]?.[2]).toEqual(expect.objectContaining({
172
+ headers: { Referer: 'https://movie.douban.com/photos/photo/2913450215/' },
173
+ timeout: 60000,
174
+ }));
175
+
176
+ expect(result).toEqual([
177
+ {
178
+ index: 2,
179
+ title: 'Character poster',
180
+ photo_id: '2913450215',
181
+ image_url: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
182
+ detail_url: 'https://movie.douban.com/photos/photo/2913450215/',
183
+ status: 'success',
184
+ size: '980 B',
185
+ },
186
+ ]);
187
+ });
188
+
189
+ it('rejects invalid subject ids before attempting browser work', async () => {
190
+ await expect(
191
+ cmd.func!({} as IPage, { id: 'movie-30382501' }),
192
+ ).rejects.toThrow('Invalid Douban subject ID');
193
+
194
+ expect(mockHttpDownload).not.toHaveBeenCalled();
195
+ });
196
+ });
@@ -0,0 +1,78 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { formatBytes } from '../../download/progress.js';
4
+ import { httpDownload, sanitizeFilename } from '../../download/index.js';
5
+ import { EmptyResultError } from '../../errors.js';
6
+ import { cli, Strategy } from '../../registry.js';
7
+ import type { DoubanSubjectPhoto, LoadDoubanSubjectPhotosOptions } from './utils.js';
8
+ import { getDoubanPhotoExtension, loadDoubanSubjectPhotos, normalizeDoubanSubjectId } from './utils.js';
9
+
10
+ function buildDoubanPhotoFilename(subjectId: string, photo: DoubanSubjectPhoto): string {
11
+ const index = String(photo.index).padStart(3, '0');
12
+ const suffix = sanitizeFilename(photo.title || photo.photoId || 'photo', 80) || 'photo';
13
+ return `${subjectId}_${index}_${photo.photoId || 'photo'}_${suffix}${getDoubanPhotoExtension(photo.imageUrl)}`;
14
+ }
15
+
16
+ cli({
17
+ site: 'douban',
18
+ name: 'download',
19
+ description: '下载电影海报/剧照图片',
20
+ domain: 'movie.douban.com',
21
+ strategy: Strategy.COOKIE,
22
+ args: [
23
+ { name: 'id', positional: true, required: true, help: '电影 subject ID' },
24
+ { name: 'type', default: 'Rb', help: '豆瓣 photos 的 type 参数,默认 Rb(海报)' },
25
+ { name: 'limit', type: 'int', default: 120, help: '最多下载多少张图片' },
26
+ { name: 'photo-id', help: '只下载指定 photo_id 的图片' },
27
+ { name: 'output', default: './douban-downloads', help: '输出目录' },
28
+ ],
29
+ columns: ['index', 'title', 'status', 'size'],
30
+ func: async (page, kwargs) => {
31
+ const subjectId = normalizeDoubanSubjectId(String(kwargs.id || ''));
32
+ const output = String(kwargs.output || './douban-downloads');
33
+ const requestedPhotoId = String(kwargs['photo-id'] || '').trim();
34
+ const loadOptions: LoadDoubanSubjectPhotosOptions = {
35
+ type: String(kwargs.type || 'Rb'),
36
+ };
37
+ if (requestedPhotoId) loadOptions.targetPhotoId = requestedPhotoId;
38
+ else loadOptions.limit = Number(kwargs.limit) || 120;
39
+
40
+ const data = await loadDoubanSubjectPhotos(page, subjectId, loadOptions);
41
+
42
+ const photos = requestedPhotoId
43
+ ? data.photos.filter((photo) => photo.photoId === requestedPhotoId)
44
+ : data.photos;
45
+
46
+ if (requestedPhotoId && !photos.length) {
47
+ throw new EmptyResultError(
48
+ 'douban download',
49
+ `Photo ID ${requestedPhotoId} was not found under subject ${subjectId}. Try "douban photos ${subjectId} -f json" first.`,
50
+ );
51
+ }
52
+
53
+ const outputDir = path.join(output, subjectId);
54
+ fs.mkdirSync(outputDir, { recursive: true });
55
+
56
+ const results: Array<Record<string, unknown>> = [];
57
+ for (const photo of photos) {
58
+ const filename = buildDoubanPhotoFilename(subjectId, photo);
59
+ const destPath = path.join(outputDir, filename);
60
+ const result = await httpDownload(photo.imageUrl, destPath, {
61
+ headers: { Referer: photo.detailUrl || `https://movie.douban.com/subject/${subjectId}/photos?type=${encodeURIComponent(String(kwargs.type || 'Rb'))}` },
62
+ timeout: 60000,
63
+ });
64
+
65
+ results.push({
66
+ index: photo.index,
67
+ title: photo.title,
68
+ photo_id: photo.photoId,
69
+ image_url: photo.imageUrl,
70
+ detail_url: photo.detailUrl,
71
+ status: result.success ? 'success' : 'failed',
72
+ size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
73
+ });
74
+ }
75
+
76
+ return results;
77
+ },
78
+ });
@@ -0,0 +1,36 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { loadDoubanSubjectPhotos, normalizeDoubanSubjectId } from './utils.js';
3
+
4
+ cli({
5
+ site: 'douban',
6
+ name: 'photos',
7
+ description: '获取电影海报/剧照图片列表',
8
+ domain: 'movie.douban.com',
9
+ strategy: Strategy.COOKIE,
10
+ args: [
11
+ { name: 'id', positional: true, required: true, help: '电影 subject ID' },
12
+ { name: 'type', default: 'Rb', help: '豆瓣 photos 的 type 参数,默认 Rb(海报)' },
13
+ { name: 'limit', type: 'int', default: 120, help: '最多返回多少张图片' },
14
+ ],
15
+ columns: ['index', 'title', 'image_url', 'detail_url'],
16
+ func: async (page, kwargs) => {
17
+ const subjectId = normalizeDoubanSubjectId(String(kwargs.id || ''));
18
+ const data = await loadDoubanSubjectPhotos(page, subjectId, {
19
+ type: String(kwargs.type || 'Rb'),
20
+ limit: Number(kwargs.limit) || 120,
21
+ });
22
+
23
+ return data.photos.map((photo) => ({
24
+ subject_id: data.subjectId,
25
+ subject_title: data.subjectTitle,
26
+ type: data.type,
27
+ index: photo.index,
28
+ photo_id: photo.photoId,
29
+ title: photo.title,
30
+ image_url: photo.imageUrl,
31
+ thumb_url: photo.thumbUrl,
32
+ detail_url: photo.detailUrl,
33
+ page: photo.page,
34
+ }));
35
+ },
36
+ });