@jackwener/opencli 1.7.12 → 1.7.13

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 (407) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12194 -6843
  4. package/clis/1point3acres/digest.js +35 -0
  5. package/clis/1point3acres/forum.js +51 -0
  6. package/clis/1point3acres/forums.js +44 -0
  7. package/clis/1point3acres/hot.js +35 -0
  8. package/clis/1point3acres/latest.js +35 -0
  9. package/clis/1point3acres/notifications.js +64 -0
  10. package/clis/1point3acres/search.js +71 -0
  11. package/clis/1point3acres/thread.js +117 -0
  12. package/clis/1point3acres/user.js +77 -0
  13. package/clis/1point3acres/utils.js +247 -0
  14. package/clis/_shared/desktop-commands.js +4 -0
  15. package/clis/aibase/news.js +110 -0
  16. package/clis/aibase/news.test.js +59 -0
  17. package/clis/amazon/discussion.test.js +1 -28
  18. package/clis/antigravity/watch.js +3 -2
  19. package/clis/arxiv/author.js +44 -0
  20. package/clis/baidu-scholar/search.js +0 -1
  21. package/clis/bbc/topic.js +57 -0
  22. package/clis/bbc/utils.js +79 -0
  23. package/clis/chaoxing/assignments.js +1 -1
  24. package/clis/chaoxing/exams.js +1 -1
  25. package/clis/chatgpt/ask.js +57 -0
  26. package/clis/chatgpt/commands.test.js +45 -0
  27. package/clis/chatgpt/detail.js +46 -0
  28. package/clis/chatgpt/history.js +39 -0
  29. package/clis/chatgpt/image.js +12 -11
  30. package/clis/chatgpt/image.test.js +23 -0
  31. package/clis/chatgpt/new.js +25 -0
  32. package/clis/chatgpt/read.js +43 -0
  33. package/clis/chatgpt/send.js +46 -0
  34. package/clis/chatgpt/status.js +29 -0
  35. package/clis/chatgpt/utils.js +294 -4
  36. package/clis/chatgpt/utils.test.js +13 -0
  37. package/clis/chatgpt-app/ask.js +6 -3
  38. package/clis/chatwise/ask.js +16 -43
  39. package/clis/chatwise/composer.test.js +186 -0
  40. package/clis/chatwise/send.js +2 -24
  41. package/clis/chatwise/utils.js +143 -0
  42. package/clis/claude/ask.js +1 -1
  43. package/clis/claude/detail.js +1 -0
  44. package/clis/claude/history.js +1 -0
  45. package/clis/claude/new.js +1 -0
  46. package/clis/claude/read.js +1 -0
  47. package/clis/claude/send.js +1 -0
  48. package/clis/claude/status.js +1 -0
  49. package/clis/codex/ask.js +15 -9
  50. package/clis/codex/history.js +16 -33
  51. package/clis/codex/projects.js +28 -0
  52. package/clis/codex/read.js +10 -4
  53. package/clis/codex/send.js +10 -3
  54. package/clis/codex/sidebar.js +356 -0
  55. package/clis/codex/sidebar.test.js +329 -0
  56. package/clis/coingecko/categories.js +75 -0
  57. package/clis/coingecko/coin.js +107 -0
  58. package/clis/coingecko/coingecko.test.js +109 -0
  59. package/clis/coingecko/derivatives.js +84 -0
  60. package/clis/coingecko/exchanges.js +74 -0
  61. package/clis/coingecko/global.js +71 -0
  62. package/clis/coingecko/top.js +64 -0
  63. package/clis/coingecko/trending.js +55 -0
  64. package/clis/coupang/add-to-cart.js +21 -13
  65. package/clis/coupang/coupang.test.js +159 -0
  66. package/clis/coupang/product.js +257 -0
  67. package/clis/coupang/search.js +38 -16
  68. package/clis/coupang/utils.js +55 -1
  69. package/clis/crates/crate.js +62 -0
  70. package/clis/crates/search.js +44 -0
  71. package/clis/crates/utils.js +72 -0
  72. package/clis/ctrip/ctrip.test.js +234 -0
  73. package/clis/ctrip/hotel-suggest.js +45 -0
  74. package/clis/ctrip/search.js +22 -68
  75. package/clis/ctrip/utils.js +175 -0
  76. package/clis/cursor/ask.js +6 -3
  77. package/clis/dblp/author.js +133 -0
  78. package/clis/dblp/venue.js +64 -0
  79. package/clis/deepseek/ask.js +12 -7
  80. package/clis/deepseek/ask.test.js +13 -13
  81. package/clis/deepseek/detail.js +38 -0
  82. package/clis/deepseek/detail.test.js +81 -0
  83. package/clis/deepseek/history.js +1 -0
  84. package/clis/deepseek/new.js +1 -0
  85. package/clis/deepseek/read.js +1 -0
  86. package/clis/deepseek/send.js +140 -0
  87. package/clis/deepseek/send.test.js +107 -0
  88. package/clis/deepseek/status.js +1 -0
  89. package/clis/deepseek/utils.js +66 -0
  90. package/clis/deepseek/utils.test.js +107 -1
  91. package/clis/defillama/defillama.test.js +99 -0
  92. package/clis/defillama/protocol.js +84 -0
  93. package/clis/defillama/protocols.js +55 -0
  94. package/clis/defillama/utils.js +99 -0
  95. package/clis/devto/latest.js +74 -0
  96. package/clis/dockerhub/image.js +52 -0
  97. package/clis/dockerhub/search.js +47 -0
  98. package/clis/dockerhub/utils.js +100 -0
  99. package/clis/doubao/ask.js +7 -3
  100. package/clis/doubao/detail.js +1 -0
  101. package/clis/doubao/history.js +1 -0
  102. package/clis/doubao/meeting-summary.js +1 -0
  103. package/clis/doubao/meeting-transcript.js +1 -0
  104. package/clis/doubao/new.js +1 -0
  105. package/clis/doubao/read.js +1 -0
  106. package/clis/doubao/send.js +1 -0
  107. package/clis/doubao/status.js +1 -0
  108. package/clis/douyin/draft.test.js +1 -30
  109. package/clis/endoflife/endoflife.test.js +51 -0
  110. package/clis/endoflife/product.js +55 -0
  111. package/clis/endoflife/utils.js +89 -0
  112. package/clis/facebook/__fixtures__/notifications-page.html +13 -0
  113. package/clis/facebook/notifications.js +326 -30
  114. package/clis/facebook/notifications.test.js +458 -0
  115. package/clis/flathub/app.js +71 -0
  116. package/clis/flathub/flathub.test.js +90 -0
  117. package/clis/flathub/search.js +80 -0
  118. package/clis/flathub/utils.js +114 -0
  119. package/clis/gemini/ask.js +7 -3
  120. package/clis/gemini/ask.test.js +2 -2
  121. package/clis/gemini/deep-research-result.js +6 -2
  122. package/clis/gemini/deep-research-result.test.js +15 -14
  123. package/clis/gemini/deep-research.js +8 -4
  124. package/clis/gemini/deep-research.test.js +15 -18
  125. package/clis/gemini/image.js +7 -2
  126. package/clis/gemini/new.js +1 -0
  127. package/clis/gemini/utils.js +0 -4
  128. package/clis/google-scholar/cite.js +0 -1
  129. package/clis/google-scholar/profile.js +0 -1
  130. package/clis/google-scholar/search.js +0 -1
  131. package/clis/goproxy/goproxy.test.js +103 -0
  132. package/clis/goproxy/module.js +47 -0
  133. package/clis/goproxy/utils.js +165 -0
  134. package/clis/goproxy/versions.js +59 -0
  135. package/clis/gov-law/recent.js +0 -1
  136. package/clis/gov-law/search.js +0 -1
  137. package/clis/gov-policy/__fixtures__/recent.html +16 -0
  138. package/clis/gov-policy/__fixtures__/search.html +41 -0
  139. package/clis/gov-policy/gov-policy.test.js +224 -0
  140. package/clis/gov-policy/recent.js +66 -24
  141. package/clis/gov-policy/search.js +65 -23
  142. package/clis/gov-policy/utils.js +54 -0
  143. package/clis/grok/ask.js +49 -265
  144. package/clis/grok/ask.test.js +21 -46
  145. package/clis/grok/detail.js +60 -0
  146. package/clis/grok/history.js +48 -0
  147. package/clis/grok/{image.ts → image.js} +56 -70
  148. package/clis/grok/image.test.ts +20 -0
  149. package/clis/grok/new.js +20 -0
  150. package/clis/grok/read.js +39 -0
  151. package/clis/grok/send.js +50 -0
  152. package/clis/grok/status.js +41 -0
  153. package/clis/grok/utils.js +326 -0
  154. package/clis/grok/utils.test.js +103 -0
  155. package/clis/hf/datasets.js +88 -0
  156. package/clis/hf/hf.test.js +16 -0
  157. package/clis/hf/models.js +91 -0
  158. package/clis/hf/paper.js +79 -0
  159. package/clis/hf/spaces.js +101 -0
  160. package/clis/hf/top.js +1 -0
  161. package/clis/homebrew/cask.js +39 -0
  162. package/clis/homebrew/formula.js +41 -0
  163. package/clis/homebrew/popular.js +54 -0
  164. package/clis/homebrew/utils.js +100 -0
  165. package/clis/hupu/__fixtures__/hot-home.html +64 -0
  166. package/clis/hupu/detail.js +0 -1
  167. package/clis/hupu/hot.js +156 -35
  168. package/clis/hupu/hot.test.js +224 -0
  169. package/clis/hupu/search.js +0 -1
  170. package/clis/instagram/note.js +1 -1
  171. package/clis/instagram/note.test.js +1 -29
  172. package/clis/instagram/post.js +1 -1
  173. package/clis/instagram/post.test.js +1 -1
  174. package/clis/instagram/reel.js +1 -1
  175. package/clis/instagram/story.js +1 -1
  176. package/clis/instagram/story.test.js +1 -34
  177. package/clis/jd/commands.test.js +1 -24
  178. package/clis/lichess/lichess.test.js +85 -0
  179. package/clis/lichess/top.js +46 -0
  180. package/clis/lichess/user.js +91 -0
  181. package/clis/lichess/utils.js +97 -0
  182. package/clis/linkedin/search.js +107 -10
  183. package/clis/linkedin/search.test.js +222 -0
  184. package/clis/linux-do/feed.js +2 -5
  185. package/clis/linux-do/feed.test.js +35 -0
  186. package/clis/lobsters/domain.js +92 -0
  187. package/clis/maven/artifact.js +49 -0
  188. package/clis/maven/search.js +51 -0
  189. package/clis/maven/utils.js +110 -0
  190. package/clis/mdn/search.js +97 -0
  191. package/clis/medium/tag.js +135 -0
  192. package/clis/npm/downloads.js +59 -0
  193. package/clis/npm/package.js +70 -0
  194. package/clis/npm/search.js +49 -0
  195. package/clis/npm/utils.js +76 -0
  196. package/clis/nuget/nuget.test.js +111 -0
  197. package/clis/nuget/package.js +101 -0
  198. package/clis/nuget/search.js +69 -0
  199. package/clis/nuget/utils.js +87 -0
  200. package/clis/nvd/cve.js +121 -0
  201. package/clis/oeis/oeis.test.js +88 -0
  202. package/clis/oeis/search.js +63 -0
  203. package/clis/oeis/sequence.js +71 -0
  204. package/clis/oeis/utils.js +88 -0
  205. package/clis/openalex/search.js +69 -0
  206. package/clis/openalex/utils.js +160 -0
  207. package/clis/openalex/work.js +65 -0
  208. package/clis/openfda/drug-label.js +74 -0
  209. package/clis/openfda/food-recall.js +65 -0
  210. package/clis/openfda/openfda.test.js +114 -0
  211. package/clis/openfda/utils.js +67 -0
  212. package/clis/osv/osv.test.js +97 -0
  213. package/clis/osv/query.js +72 -0
  214. package/clis/osv/utils.js +169 -0
  215. package/clis/osv/vulnerability.js +54 -0
  216. package/clis/packagist/package.js +49 -0
  217. package/clis/packagist/search.js +43 -0
  218. package/clis/packagist/utils.js +113 -0
  219. package/clis/paperreview/feedback.js +1 -1
  220. package/clis/paperreview/review.js +1 -1
  221. package/clis/paperreview/submit.js +1 -1
  222. package/clis/pixiv/download.test.js +1 -1
  223. package/clis/pixiv/illusts.test.js +1 -1
  224. package/clis/pixiv/search.test.js +1 -1
  225. package/clis/pubmed/article.js +50 -0
  226. package/clis/pubmed/author.js +64 -0
  227. package/clis/pubmed/citations.js +36 -0
  228. package/clis/pubmed/pubmed.test.js +276 -0
  229. package/clis/pubmed/related.js +45 -0
  230. package/clis/pubmed/search.js +75 -0
  231. package/clis/pubmed/utils.js +309 -0
  232. package/clis/pypi/downloads.js +66 -0
  233. package/clis/pypi/package.js +79 -0
  234. package/clis/pypi/utils.js +55 -0
  235. package/clis/quark/mv.js +1 -1
  236. package/clis/quark/save.js +1 -1
  237. package/clis/qwen/ask.js +85 -0
  238. package/clis/qwen/detail.js +62 -0
  239. package/clis/qwen/history.js +61 -0
  240. package/clis/qwen/image.js +179 -0
  241. package/clis/qwen/new.js +23 -0
  242. package/clis/qwen/read.js +41 -0
  243. package/clis/qwen/send.js +55 -0
  244. package/clis/qwen/status.js +37 -0
  245. package/clis/qwen/utils.js +409 -0
  246. package/clis/qwen/utils.test.js +45 -0
  247. package/clis/rest-countries/country.js +65 -0
  248. package/clis/rest-countries/region.js +64 -0
  249. package/clis/rest-countries/rest-countries.test.js +83 -0
  250. package/clis/rest-countries/utils.js +126 -0
  251. package/clis/reuters/article-detail.js +53 -0
  252. package/clis/reuters/reuters.test.js +299 -0
  253. package/clis/reuters/search.js +45 -34
  254. package/clis/reuters/utils.js +159 -0
  255. package/clis/rfc/rfc.js +52 -0
  256. package/clis/rfc/rfc.test.js +74 -0
  257. package/clis/rfc/utils.js +72 -0
  258. package/clis/rubygems/gem.js +42 -0
  259. package/clis/rubygems/search.js +47 -0
  260. package/clis/rubygems/utils.js +86 -0
  261. package/clis/stackoverflow/related.js +66 -0
  262. package/clis/stackoverflow/stackoverflow.test.js +58 -0
  263. package/clis/stackoverflow/tag.js +60 -0
  264. package/clis/stackoverflow/user.js +50 -0
  265. package/clis/stackoverflow/utils.js +118 -0
  266. package/clis/steam/app.js +67 -0
  267. package/clis/steam/search.js +58 -0
  268. package/clis/steam/steam.test.js +46 -0
  269. package/clis/steam/utils.js +107 -0
  270. package/clis/taobao/commands.test.js +1 -24
  271. package/clis/test-utils.js +61 -0
  272. package/clis/tieba/hot.js +0 -1
  273. package/clis/tiktok/comment.js +128 -41
  274. package/clis/tiktok/creator-videos.js +270 -0
  275. package/clis/tiktok/creator-videos.test.js +113 -0
  276. package/clis/tiktok/explore.js +137 -29
  277. package/clis/tiktok/follow.js +115 -33
  278. package/clis/tiktok/following.js +157 -36
  279. package/clis/tiktok/friends.js +139 -37
  280. package/clis/tiktok/live.js +137 -41
  281. package/clis/tiktok/notifications.js +141 -38
  282. package/clis/tiktok/refactor.test.js +389 -0
  283. package/clis/tiktok/unfollow.js +124 -38
  284. package/clis/tiktok/user.js +203 -29
  285. package/clis/tiktok/utils.js +505 -0
  286. package/clis/tiktok/write-refactor.test.js +370 -0
  287. package/clis/toutiao/articles.js +36 -62
  288. package/clis/toutiao/hot.js +63 -0
  289. package/clis/toutiao/toutiao.test.js +378 -0
  290. package/clis/toutiao/utils.js +161 -0
  291. package/clis/tvmaze/search.js +61 -0
  292. package/clis/tvmaze/show.js +60 -0
  293. package/clis/tvmaze/tvmaze.test.js +93 -0
  294. package/clis/tvmaze/utils.js +110 -0
  295. package/clis/twitter/accept.js +1 -1
  296. package/clis/twitter/followers.js +134 -69
  297. package/clis/twitter/reply-dm.js +1 -1
  298. package/clis/twitter/reply.test.js +1 -29
  299. package/clis/uisdc/news.js +105 -0
  300. package/clis/uisdc/news.test.js +66 -0
  301. package/clis/wanfang/search.js +0 -1
  302. package/clis/web/read.js +47 -17
  303. package/clis/web/read.test.js +101 -1
  304. package/clis/weixin/create-draft.js +1 -1
  305. package/clis/weixin/drafts.js +1 -1
  306. package/clis/weixin/drafts.test.js +5 -1
  307. package/clis/weixin/search.js +157 -0
  308. package/clis/weixin/search.test.js +227 -0
  309. package/clis/wikidata/entity.js +60 -0
  310. package/clis/wikidata/search.js +50 -0
  311. package/clis/wikidata/utils.js +117 -0
  312. package/clis/wikidata/wikidata.test.js +83 -0
  313. package/clis/wikipedia/page.js +95 -0
  314. package/clis/wttr/current.js +63 -0
  315. package/clis/wttr/forecast.js +71 -0
  316. package/clis/wttr/utils.js +50 -0
  317. package/clis/wttr/wttr.test.js +84 -0
  318. package/clis/xianyu/chat.js +16 -4
  319. package/clis/xianyu/chat.test.js +64 -0
  320. package/clis/xianyu/publish.js +485 -0
  321. package/clis/xianyu/publish.test.js +220 -0
  322. package/clis/xiaoe/catalog.js +105 -40
  323. package/clis/xiaoe/content.js +164 -29
  324. package/clis/xiaoe/courses.js +86 -29
  325. package/clis/xiaoe/xiaoe.test.js +486 -0
  326. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  327. package/clis/xiaohongshu/publish.js +16 -3
  328. package/clis/xiaohongshu/publish.test.js +46 -1
  329. package/clis/youtube/transcript.js +13 -19
  330. package/clis/youtube/transcript.test.js +17 -0
  331. package/clis/yuanbao/ask.js +17 -66
  332. package/clis/yuanbao/ask.test.js +5 -5
  333. package/clis/yuanbao/detail.js +65 -0
  334. package/clis/yuanbao/history.js +51 -0
  335. package/clis/yuanbao/new.js +1 -0
  336. package/clis/yuanbao/read.js +38 -0
  337. package/clis/yuanbao/send.js +57 -0
  338. package/clis/yuanbao/shared.js +297 -5
  339. package/clis/yuanbao/shared.test.js +80 -0
  340. package/clis/yuanbao/status.js +44 -0
  341. package/clis/zlibrary/commands.test.js +1 -11
  342. package/dist/src/browser/base-page.d.ts +9 -0
  343. package/dist/src/browser/base-page.js +44 -1
  344. package/dist/src/browser/base-page.test.js +66 -0
  345. package/dist/src/browser/cdp.d.ts +1 -0
  346. package/dist/src/browser/cdp.js +51 -9
  347. package/dist/src/browser/daemon-client.d.ts +4 -0
  348. package/dist/src/browser/errors.js +1 -1
  349. package/dist/src/browser/page.d.ts +1 -1
  350. package/dist/src/browser/page.js +3 -1
  351. package/dist/src/browser/page.test.js +29 -0
  352. package/dist/src/browser/target-errors.d.ts +2 -1
  353. package/dist/src/browser/target-errors.js +1 -0
  354. package/dist/src/browser/target-resolver.d.ts +25 -0
  355. package/dist/src/browser/target-resolver.js +43 -0
  356. package/dist/src/build-manifest.js +9 -4
  357. package/dist/src/build-manifest.test.js +2 -8
  358. package/dist/src/capabilityRouting.d.ts +16 -1
  359. package/dist/src/capabilityRouting.js +24 -1
  360. package/dist/src/capabilityRouting.test.js +19 -1
  361. package/dist/src/cli.js +76 -11
  362. package/dist/src/cli.test.js +150 -0
  363. package/dist/src/commanderAdapter.js +0 -5
  364. package/dist/src/commanderAdapter.test.js +0 -1
  365. package/dist/src/discovery.js +2 -5
  366. package/dist/src/errors.js +1 -1
  367. package/dist/src/execution.d.ts +1 -1
  368. package/dist/src/execution.js +111 -27
  369. package/dist/src/execution.test.js +326 -17
  370. package/dist/src/help.d.ts +23 -2
  371. package/dist/src/help.js +41 -19
  372. package/dist/src/help.test.d.ts +1 -0
  373. package/dist/src/help.test.js +54 -0
  374. package/dist/src/main.js +14 -1
  375. package/dist/src/manifest-types.d.ts +5 -3
  376. package/dist/src/pipeline/executor.js +1 -1
  377. package/dist/src/pipeline/executor.test.js +8 -0
  378. package/dist/src/pipeline/registry.d.ts +9 -0
  379. package/dist/src/pipeline/registry.js +13 -1
  380. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  381. package/dist/src/pipeline/steps/browser.js +10 -0
  382. package/dist/src/pipeline/steps/download.test.js +1 -0
  383. package/dist/src/registry-api.d.ts +1 -1
  384. package/dist/src/registry.d.ts +12 -11
  385. package/dist/src/registry.js +16 -6
  386. package/dist/src/registry.test.js +2 -2
  387. package/dist/src/runtime.d.ts +2 -1
  388. package/dist/src/runtime.js +1 -1
  389. package/dist/src/serialization.d.ts +2 -2
  390. package/dist/src/serialization.js +4 -6
  391. package/dist/src/serialization.test.js +17 -0
  392. package/dist/src/types.d.ts +17 -0
  393. package/dist/src/validate.js +15 -11
  394. package/dist/src/validate.test.d.ts +9 -0
  395. package/dist/src/validate.test.js +90 -0
  396. package/package.json +1 -1
  397. package/scripts/fetch-adapters.js +1 -1
  398. package/scripts/typed-error-lint-baseline.json +5 -77
  399. package/clis/ctrip/search.test.js +0 -64
  400. package/clis/gov-policy/commands.test.js +0 -27
  401. package/clis/linux-do/category.js +0 -37
  402. package/clis/linux-do/hot.js +0 -26
  403. package/clis/linux-do/latest.js +0 -19
  404. package/clis/pixiv/test-utils.js +0 -23
  405. package/clis/toutiao/articles.test.js +0 -30
  406. package/dist/src/analysis.d.ts +0 -40
  407. package/dist/src/analysis.js +0 -172
@@ -1,36 +1,144 @@
1
- import { cli } from '@jackwener/opencli/registry';
2
- cli({
1
+ // Browse TikTok "For You" / explore feed via page-context API.
2
+ //
3
+ // Replaces legacy DOM-link scraping (`querySelectorAll('a[href*="/video/"]')`).
4
+ // We borrow the live page session: read `__UNIVERSAL_DATA_FOR_REHYDRATION__` for
5
+ // the warm `webapp.recommend-feed` snapshot, then extend with `/api/recommend/item_list/`
6
+ // when a larger `--limit` is requested. Stays inside the browser, so the session
7
+ // cookies + msToken go along automatically.
8
+
9
+ import { cli, Strategy } from '@jackwener/opencli/registry';
10
+ import {
11
+ EmptyResultError,
12
+ } from '@jackwener/opencli/errors';
13
+ import {
14
+ BROWSER_HELPERS,
15
+ MAX_PAGES,
16
+ SERVER_PAGE_MAX,
17
+ TIKTOK_AID,
18
+ requireLimit,
19
+ throwTikTokPageContextError,
20
+ VIDEO_ITEM_NORMALIZER,
21
+ } from './utils.js';
22
+
23
+ const DEFAULT_LIMIT = 20;
24
+ const MAX_LIMIT = 120;
25
+
26
+ function buildExploreScript(limit) {
27
+ return `
28
+ (async () => {
29
+ const limit = ${Number(limit)};
30
+ const maxPages = ${MAX_PAGES};
31
+ const SERVER_PAGE_MAX = ${SERVER_PAGE_MAX};
32
+ // limit was validated upfront (requireLimit). pageSize is the per-request
33
+ // cap TikTok accepts; we still loop until \`dedup.size < limit\` is satisfied.
34
+ const pageSize = limit < SERVER_PAGE_MAX ? limit : SERVER_PAGE_MAX;
35
+ const aid = ${JSON.stringify(TIKTOK_AID)};
36
+
37
+ ${BROWSER_HELPERS}
38
+ ${VIDEO_ITEM_NORMALIZER}
39
+
40
+ function collectFromState(root) {
41
+ if (!root) return [];
42
+ const out = [];
43
+ walkObjects(root, (node) => {
44
+ if (Array.isArray(node)) return false;
45
+ if (Array.isArray(node.itemList)) {
46
+ for (const item of node.itemList) out.push(item);
47
+ }
48
+ if (Array.isArray(node.items)) {
49
+ for (const item of node.items) out.push(item);
50
+ }
51
+ return false;
52
+ });
53
+ return out;
54
+ }
55
+
56
+ const dedup = new Map();
57
+ const universal = findUniversalData();
58
+ for (const item of collectFromState(universal)) {
59
+ const row = normalizeVideoItem(item, dedup.size + 1);
60
+ if (row && !dedup.has(row.id)) dedup.set(row.id, row);
61
+ }
62
+
63
+ const msToken = getCookie('msToken');
64
+ let apiFailure = null;
65
+ if (dedup.size < limit) {
66
+ let cursor = 0;
67
+ for (let page = 0; page < maxPages && dedup.size < limit; page += 1) {
68
+ const params = new URLSearchParams({
69
+ aid,
70
+ count: String(pageSize),
71
+ from_page: 'fyp',
72
+ cursor: String(cursor),
73
+ });
74
+ if (msToken) params.set('msToken', msToken);
75
+ try {
76
+ const data = await fetchJson('/api/recommend/item_list/?' + params.toString());
77
+ assertTikTokApiSuccess(data, 'recommend');
78
+ const items = Array.isArray(data.itemList) ? data.itemList : [];
79
+ for (const item of items) {
80
+ const row = normalizeVideoItem(item, dedup.size + 1);
81
+ if (row && !dedup.has(row.id)) dedup.set(row.id, row);
82
+ }
83
+ if (data.hasMore !== true && items.length === 0) break;
84
+ cursor = asNumber(data.cursor) ?? cursor + items.length;
85
+ } catch (error) {
86
+ apiFailure = error instanceof Error ? error.message : String(error);
87
+ break;
88
+ }
89
+ }
90
+ }
91
+
92
+ const rows = Array.from(dedup.values())
93
+ .slice(0, limit)
94
+ .map((row, index) => ({ ...row, index: index + 1 }));
95
+
96
+ if (rows.length === 0) {
97
+ const suffix = apiFailure ? ' (recommend API failed: ' + apiFailure + ')' : '';
98
+ throw new Error('No videos found on /explore' + suffix);
99
+ }
100
+ return rows;
101
+ })()
102
+ `;
103
+ }
104
+
105
+ async function listExploreVideos(page, args) {
106
+ const limit = requireLimit(args.limit, { fallback: DEFAULT_LIMIT, max: MAX_LIMIT });
107
+ await page.goto('https://www.tiktok.com/explore', { waitUntil: 'load', settleMs: 5000 });
108
+ let rows;
109
+ try {
110
+ rows = await page.evaluate(buildExploreScript(limit));
111
+ } catch (error) {
112
+ throwTikTokPageContextError(error, {
113
+ authMessage: 'TikTok requires browser access to load the explore feed',
114
+ emptyPattern: /No videos found/,
115
+ emptyTarget: 'tiktok explore',
116
+ failureMessage: 'Failed to load TikTok explore feed',
117
+ });
118
+ }
119
+ if (!Array.isArray(rows) || rows.length === 0) {
120
+ throw new EmptyResultError('tiktok explore', 'TikTok returned an empty recommend feed');
121
+ }
122
+ return rows;
123
+ }
124
+
125
+ export const exploreCommand = cli({
3
126
  site: 'tiktok',
4
127
  name: 'explore',
5
128
  access: 'read',
6
- description: 'Get trending TikTok videos from explore page',
129
+ description: 'Get trending TikTok videos from the recommend feed via page-context APIs',
7
130
  domain: 'www.tiktok.com',
131
+ strategy: Strategy.COOKIE,
132
+ browser: true,
8
133
  args: [
9
- { name: 'limit', type: 'int', default: 20, help: 'Number of videos' },
10
- ],
11
- columns: ['rank', 'author', 'views', 'url'],
12
- pipeline: [
13
- { navigate: { url: 'https://www.tiktok.com/explore', settleMs: 5000 } },
14
- { evaluate: `(() => {
15
- const limit = \${{ args.limit }};
16
- const links = Array.from(document.querySelectorAll('a[href*="/video/"]'));
17
- const seen = new Set();
18
- const results = [];
19
- for (const a of links) {
20
- const href = a.href;
21
- if (seen.has(href)) continue;
22
- seen.add(href);
23
- const match = href.match(/@([^/]+)\\/video\\/(\\d+)/);
24
- results.push({
25
- rank: results.length + 1,
26
- author: match ? match[1] : '',
27
- views: a.textContent.trim() || '-',
28
- url: href,
29
- });
30
- if (results.length >= limit) break;
31
- }
32
- return results;
33
- })()
34
- ` },
134
+ { name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of videos to return (max ${MAX_LIMIT})` },
35
135
  ],
136
+ columns: ['index', 'id', 'author', 'url', 'cover', 'title', 'desc', 'plays', 'likes', 'comments', 'shares', 'createTime'],
137
+ func: listExploreVideos,
36
138
  });
139
+
140
+ export const __test__ = {
141
+ buildExploreScript,
142
+ DEFAULT_LIMIT,
143
+ MAX_LIMIT,
144
+ };
@@ -1,40 +1,122 @@
1
- import { cli } from '@jackwener/opencli/registry';
2
- cli({
1
+ // Follow a TikTok user via in-page button click + state verification.
2
+ //
3
+ // Replaces the legacy pipeline-based adapter that returned silent failure
4
+ // rows like `{ status: 'Follow button not found', username }`. Route 1
5
+ // here is conservative: keep the live UI button as the trigger (TikTok's
6
+ // `/api/commit/follow/user/` requires X-Bogus signing — out of scope for
7
+ // this PR), but harden every transition with typed errors:
8
+ //
9
+ // ArgumentError — empty or malformed username
10
+ // AuthRequiredError — not logged in (no sessionid + no viewer secUid)
11
+ // CommandExecutionError — button missing / state verification fails /
12
+ // rate limit detected (retryable=true since
13
+ // TikTok dedupes follow on retry)
14
+ //
15
+ // `result` row enum: `followed` / `already-following` / `already-friends`
16
+ // (mutual). Failures throw — never returned as a success row.
17
+
18
+ import { cli, Strategy } from '@jackwener/opencli/registry';
19
+ import {
20
+ BROWSER_HELPERS,
21
+ BUTTON_WALKER_HELPERS,
22
+ RETRYABLE_HINTS,
23
+ TIKTOK_HOST,
24
+ normalizeUsername,
25
+ throwButtonWalkerError,
26
+ } from './utils.js';
27
+
28
+ function buildFollowScript(username) {
29
+ return `
30
+ (async () => {
31
+ const username = ${JSON.stringify(username)};
32
+
33
+ ${BROWSER_HELPERS}
34
+ ${BUTTON_WALKER_HELPERS}
35
+
36
+ ensureLoggedInOrThrow();
37
+ ensureNoRateLimitOrThrow();
38
+
39
+ const FOLLOW_LABELS = ['Follow', '关注', 'フォロー'];
40
+ const FOLLOWING_LABELS = ['Following', '已关注', 'フォロー中'];
41
+ const FRIENDS_LABELS = ['Friends', '互关', 'フレンド'];
42
+ const ALREADY_LABELS = FOLLOWING_LABELS.concat(FRIENDS_LABELS);
43
+
44
+ // Idempotent fast path: already in target state.
45
+ if (buttonExists(FRIENDS_LABELS)) {
46
+ return [{ username, url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username), result: 'already-friends' }];
47
+ }
48
+ if (buttonExists(FOLLOWING_LABELS)) {
49
+ return [{ username, url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username), result: 'already-following' }];
50
+ }
51
+
52
+ const followBtn = findButtonByText(FOLLOW_LABELS);
53
+ if (!followBtn) {
54
+ throw new Error('BUTTON_NOT_FOUND: Follow button not on profile page (logged out, private account, or selectors changed)');
55
+ }
56
+
57
+ const target = followBtn.closest('button') || followBtn.closest('[role="button"]') || followBtn;
58
+ target.click();
59
+
60
+ // State verification: button text should flip to Following / 已关注 etc.
61
+ const flipped = await waitFor(() => buttonExists(ALREADY_LABELS), { timeoutMs: 5000 });
62
+ if (!flipped) {
63
+ ensureNoRateLimitOrThrow();
64
+ throw new Error('STATE_VERIFY_FAIL: follow button did not flip to Following within 5s; relation may not have been recorded');
65
+ }
66
+
67
+ // Re-check rate limit AFTER click — TikTok sometimes flashes captcha
68
+ // mid-flight even when the button text appears to flip.
69
+ ensureNoRateLimitOrThrow();
70
+
71
+ return [{
72
+ username,
73
+ url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username),
74
+ result: 'followed',
75
+ }];
76
+ })()
77
+ `;
78
+ }
79
+
80
+ async function followUser(page, args) {
81
+ const username = normalizeUsername(args.username);
82
+ const throwFailure = (error) => throwButtonWalkerError(error, {
83
+ authMessage: 'TikTok requires login to follow users',
84
+ failureMessage: `Failed to follow @${username}`,
85
+ retryableHint: RETRYABLE_HINTS.relationFailure,
86
+ });
87
+ let rows;
88
+ try {
89
+ await page.goto(`${TIKTOK_HOST}/@${encodeURIComponent(username)}`, {
90
+ waitUntil: 'load',
91
+ settleMs: 5000,
92
+ });
93
+ rows = await page.evaluate(buildFollowScript(username));
94
+ } catch (error) {
95
+ throwFailure(error);
96
+ }
97
+ if (!Array.isArray(rows) || rows.length === 0) {
98
+ // Defensive: build script always returns a row on success path.
99
+ // If we land here, treat as state-verify failure.
100
+ throwFailure(new Error(`STATE_VERIFY_FAIL: follow returned no row for @${username}`));
101
+ }
102
+ return rows;
103
+ }
104
+
105
+ export const followCommand = cli({
3
106
  site: 'tiktok',
4
107
  name: 'follow',
5
108
  access: 'write',
6
- description: 'Follow a TikTok user',
109
+ description: 'Follow a TikTok user by username',
7
110
  domain: 'www.tiktok.com',
111
+ strategy: Strategy.COOKIE,
112
+ browser: true,
8
113
  args: [
9
- {
10
- name: 'username',
11
- required: true,
12
- positional: true,
13
- help: 'TikTok username (without @)',
14
- },
15
- ],
16
- columns: ['status', 'username'],
17
- pipeline: [
18
- { navigate: { url: 'https://www.tiktok.com/@${{ args.username }}', settleMs: 6000 } },
19
- { evaluate: `(async () => {
20
- const username = \${{ args.username | json }};
21
- const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
22
- const followBtn = buttons.find(function(b) {
23
- var text = b.textContent.trim();
24
- return text === 'Follow' || text === '关注';
25
- });
26
- if (!followBtn) {
27
- var isFollowing = buttons.some(function(b) {
28
- var t = b.textContent.trim();
29
- return t === 'Following' || t === '已关注' || t === 'Friends' || t === '互关';
30
- });
31
- if (isFollowing) return [{ status: 'Already following', username: username }];
32
- return [{ status: 'Follow button not found', username: username }];
33
- }
34
- followBtn.click();
35
- await new Promise(r => setTimeout(r, 2000));
36
- return [{ status: 'Followed', username: username }];
37
- })()
38
- ` },
114
+ { name: 'username', required: true, positional: true, help: 'TikTok username (without @)' },
39
115
  ],
116
+ columns: ['username', 'url', 'result'],
117
+ func: followUser,
40
118
  });
119
+
120
+ export const __test__ = {
121
+ buildFollowScript,
122
+ };
@@ -1,43 +1,164 @@
1
- import { cli } from '@jackwener/opencli/registry';
2
- cli({
3
- site: 'tiktok',
4
- name: 'following',
5
- access: 'read',
6
- description: 'List accounts you follow on TikTok',
7
- domain: 'www.tiktok.com',
8
- args: [
9
- { name: 'limit', type: 'int', default: 20, help: 'Number of accounts' },
10
- ],
11
- columns: ['index', 'username', 'name'],
12
- pipeline: [
13
- { navigate: { url: 'https://www.tiktok.com/following', settleMs: 5000 } },
14
- { evaluate: `(() => {
15
- const limit = \${{ args.limit }};
16
- const links = Array.from(document.querySelectorAll('a[href*="/@"]'))
17
- .filter(function(a) {
18
- const text = a.textContent.trim();
19
- return text.length > 1 && text.length < 80 &&
20
- !text.includes('Profile') && !text.includes('More') && !text.includes('Upload');
1
+ // List the accounts the logged-in user follows via page-context API.
2
+ //
3
+ // Replaces legacy DOM-link scraping which mistakenly hoovered up navigation
4
+ // links and "Profile" / "Upload" labels. We resolve the viewer's `secUid` from
5
+ // the warm `__UNIVERSAL_DATA_FOR_REHYDRATION__` snapshot, then page through
6
+ // `/api/user/list/?scene=21` (TikTok's own following endpoint).
7
+
8
+ import { cli, Strategy } from '@jackwener/opencli/registry';
9
+ import {
10
+ EmptyResultError,
11
+ } from '@jackwener/opencli/errors';
12
+ import {
13
+ BROWSER_HELPERS,
14
+ MAX_PAGES,
15
+ SERVER_PAGE_MAX,
16
+ TIKTOK_AID,
17
+ USER_ITEM_NORMALIZER,
18
+ requireLimit,
19
+ throwTikTokPageContextError,
20
+ } from './utils.js';
21
+
22
+ const DEFAULT_LIMIT = 20;
23
+ const MAX_LIMIT = 200;
24
+
25
+ function buildFollowingScript(limit) {
26
+ return `
27
+ (async () => {
28
+ const limit = ${Number(limit)};
29
+ const maxPages = ${MAX_PAGES * 2};
30
+ const SERVER_PAGE_MAX = ${SERVER_PAGE_MAX};
31
+ const pageSize = limit < SERVER_PAGE_MAX ? limit : SERVER_PAGE_MAX;
32
+ const aid = ${JSON.stringify(TIKTOK_AID)};
33
+
34
+ ${BROWSER_HELPERS}
35
+ ${USER_ITEM_NORMALIZER}
36
+
37
+ function findViewerSecUid(root) {
38
+ if (!root) return '';
39
+ let found = '';
40
+ walkObjects(root, (node) => {
41
+ if (Array.isArray(node)) return false;
42
+ const candidate = node?.user;
43
+ if (candidate && typeof candidate === 'object') {
44
+ const secUid = String(candidate.secUid || candidate.sec_uid || '').trim();
45
+ const isMe = Boolean(candidate.isOwner || candidate.is_owner || candidate.isCurrentUser);
46
+ if (secUid && isMe) {
47
+ found = secUid;
48
+ return true;
49
+ }
50
+ }
51
+ const direct = node?.userInfo?.user;
52
+ if (direct && (direct.isOwner || direct.is_owner)) {
53
+ const secUid = String(direct.secUid || direct.sec_uid || '').trim();
54
+ if (secUid) {
55
+ found = secUid;
56
+ return true;
57
+ }
58
+ }
59
+ return false;
21
60
  });
61
+ return found;
62
+ }
63
+
64
+ const universal = findUniversalData();
65
+ let viewerSecUid = findViewerSecUid(universal);
66
+ const msToken = getCookie('msToken');
22
67
 
23
- const seen = {};
24
- const results = [];
25
- for (const a of links) {
26
- const match = a.href.match(/@([^/]+)/);
27
- const username = match ? match[1] : '';
28
- if (!username || seen[username]) continue;
29
- seen[username] = true;
30
- const raw = a.textContent.trim();
31
- const name = raw.replace(username, '').replace('@', '').trim();
32
- results.push({
33
- index: results.length + 1,
34
- username: username,
35
- name: name || username,
68
+ if (!viewerSecUid) {
69
+ try {
70
+ const me = await fetchJson('/api/user/info/?aid=' + aid + (msToken ? '&msToken=' + encodeURIComponent(msToken) : ''));
71
+ viewerSecUid = String(me?.userInfo?.user?.secUid || me?.user?.secUid || me?.userInfo?.secUid || '').trim();
72
+ } catch {
73
+ // Fall through to typed AUTH_REQUIRED below.
74
+ }
75
+ }
76
+ if (!viewerSecUid) {
77
+ throw new Error('AUTH_REQUIRED: cannot resolve viewer secUid (login required)');
78
+ }
79
+
80
+ const dedup = new Map();
81
+ let apiFailure = null;
82
+ let cursor = 0;
83
+ for (let page = 0; page < maxPages && dedup.size < limit; page += 1) {
84
+ const params = new URLSearchParams({
85
+ aid,
86
+ scene: '21',
87
+ secUid: viewerSecUid,
88
+ count: String(pageSize),
89
+ minCursor: String(cursor),
90
+ maxCursor: '0',
36
91
  });
37
- if (results.length >= limit) break;
92
+ if (msToken) params.set('msToken', msToken);
93
+ try {
94
+ const data = await fetchJson('/api/user/list/?' + params.toString());
95
+ assertTikTokApiSuccess(data, 'user-list');
96
+ const list = Array.isArray(data.userList)
97
+ ? data.userList
98
+ : (Array.isArray(data.user_list) ? data.user_list : []);
99
+ if (list.length === 0) break;
100
+ for (const entry of list) {
101
+ const row = normalizeUserRow(entry?.user || entry, dedup.size + 1);
102
+ if (row && !dedup.has(row.username)) dedup.set(row.username, row);
103
+ }
104
+ if (data.hasMore !== true) break;
105
+ cursor = asNumber(data.minCursor) ?? asNumber(data.cursor) ?? cursor + list.length;
106
+ } catch (error) {
107
+ apiFailure = error instanceof Error ? error.message : String(error);
108
+ break;
109
+ }
38
110
  }
39
- return results;
111
+
112
+ const rows = Array.from(dedup.values())
113
+ .slice(0, limit)
114
+ .map((row, index) => ({ ...row, index: index + 1 }));
115
+
116
+ if (rows.length === 0) {
117
+ const suffix = apiFailure ? ' (user-list API failed: ' + apiFailure + ')' : '';
118
+ throw new Error('No following entries returned' + suffix);
119
+ }
120
+ return rows;
40
121
  })()
41
- ` },
122
+ `;
123
+ }
124
+
125
+ async function listFollowing(page, args) {
126
+ const limit = requireLimit(args.limit, { fallback: DEFAULT_LIMIT, max: MAX_LIMIT });
127
+ await page.goto('https://www.tiktok.com/following', { waitUntil: 'load', settleMs: 5000 });
128
+ let rows;
129
+ try {
130
+ rows = await page.evaluate(buildFollowingScript(limit));
131
+ } catch (error) {
132
+ throwTikTokPageContextError(error, {
133
+ authMessage: 'TikTok requires login to read your following list',
134
+ emptyPattern: /No following entries/,
135
+ emptyTarget: 'tiktok following',
136
+ failureMessage: 'Failed to load TikTok following list',
137
+ });
138
+ }
139
+ if (!Array.isArray(rows) || rows.length === 0) {
140
+ throw new EmptyResultError('tiktok following', 'TikTok returned no following entries');
141
+ }
142
+ return rows;
143
+ }
144
+
145
+ export const followingCommand = cli({
146
+ site: 'tiktok',
147
+ name: 'following',
148
+ access: 'read',
149
+ description: 'List accounts the logged-in user follows on TikTok via page-context APIs',
150
+ domain: 'www.tiktok.com',
151
+ strategy: Strategy.COOKIE,
152
+ browser: true,
153
+ args: [
154
+ { name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of accounts (max ${MAX_LIMIT})` },
42
155
  ],
156
+ columns: ['index', 'username', 'name', 'secUid', 'verified', 'followers', 'following', 'url'],
157
+ func: listFollowing,
43
158
  });
159
+
160
+ export const __test__ = {
161
+ buildFollowingScript,
162
+ DEFAULT_LIMIT,
163
+ MAX_LIMIT,
164
+ };