@jackwener/opencli 1.1.0 → 1.1.1

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 (354) hide show
  1. package/.agents/skills/cross-project-adapter-migration/SKILL.md +2 -2
  2. package/.github/pull_request_template.md +7 -0
  3. package/.github/workflows/doc-check.yml +36 -0
  4. package/.github/workflows/docs.yml +7 -42
  5. package/CHANGELOG.md +23 -0
  6. package/CLI-EXPLORER.md +9 -8
  7. package/README.md +25 -10
  8. package/README.zh-CN.md +26 -11
  9. package/SKILL.md +95 -31
  10. package/dist/browser/cdp.js +6 -1
  11. package/dist/browser/page.d.ts +4 -1
  12. package/dist/browser/page.js +7 -1
  13. package/dist/build-manifest.js +23 -16
  14. package/dist/cli-manifest.json +431 -276
  15. package/dist/cli.d.ts +6 -0
  16. package/dist/cli.js +189 -162
  17. package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
  18. package/dist/clis/apple-podcasts/commands.test.js +76 -0
  19. package/dist/clis/apple-podcasts/search.js +2 -2
  20. package/dist/clis/apple-podcasts/top.js +9 -2
  21. package/dist/clis/arxiv/search.js +1 -1
  22. package/dist/clis/bilibili/dynamic.js +1 -1
  23. package/dist/clis/bilibili/favorite.js +1 -1
  24. package/dist/clis/bilibili/feed.js +1 -1
  25. package/dist/clis/bilibili/following.js +1 -1
  26. package/dist/clis/bilibili/history.js +1 -1
  27. package/dist/clis/bilibili/me.js +1 -1
  28. package/dist/clis/bilibili/ranking.js +1 -1
  29. package/dist/clis/bilibili/search.js +3 -3
  30. package/dist/clis/bilibili/subtitle.js +1 -1
  31. package/dist/clis/bilibili/user-videos.js +1 -1
  32. package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
  33. package/dist/clis/bloomberg/businessweek.js +17 -0
  34. package/dist/clis/bloomberg/economics.js +17 -0
  35. package/dist/clis/bloomberg/feeds.d.ts +1 -0
  36. package/dist/clis/bloomberg/feeds.js +15 -0
  37. package/dist/clis/bloomberg/industries.d.ts +1 -0
  38. package/dist/clis/bloomberg/industries.js +17 -0
  39. package/dist/clis/bloomberg/main.d.ts +1 -0
  40. package/dist/clis/bloomberg/main.js +17 -0
  41. package/dist/clis/bloomberg/markets.d.ts +1 -0
  42. package/dist/clis/bloomberg/markets.js +17 -0
  43. package/dist/clis/bloomberg/news.d.ts +1 -0
  44. package/dist/clis/bloomberg/news.js +105 -0
  45. package/dist/clis/bloomberg/opinions.d.ts +1 -0
  46. package/dist/clis/bloomberg/opinions.js +17 -0
  47. package/dist/clis/bloomberg/politics.d.ts +1 -0
  48. package/dist/clis/bloomberg/politics.js +17 -0
  49. package/dist/clis/bloomberg/tech.d.ts +1 -0
  50. package/dist/clis/bloomberg/tech.js +17 -0
  51. package/dist/clis/bloomberg/utils.d.ts +34 -0
  52. package/dist/clis/bloomberg/utils.js +364 -0
  53. package/dist/clis/bloomberg/utils.test.d.ts +1 -0
  54. package/dist/clis/bloomberg/utils.test.js +129 -0
  55. package/dist/clis/boss/batchgreet.js +2 -2
  56. package/dist/clis/boss/chatlist.js +2 -2
  57. package/dist/clis/boss/detail.js +2 -2
  58. package/dist/clis/boss/greet.js +4 -4
  59. package/dist/clis/boss/search.js +1 -1
  60. package/dist/clis/boss/send.js +1 -1
  61. package/dist/clis/boss/stats.js +2 -2
  62. package/dist/clis/chaoxing/assignments.js +1 -1
  63. package/dist/clis/chaoxing/exams.js +1 -1
  64. package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
  65. package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
  66. package/dist/clis/chaoxing/utils.test.d.ts +1 -0
  67. package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
  68. package/dist/clis/chatgpt/read.js +1 -1
  69. package/dist/clis/chatwise/export.js +1 -1
  70. package/dist/clis/chatwise/model.js +2 -2
  71. package/dist/clis/chatwise/screenshot.js +1 -1
  72. package/dist/clis/codex/export.js +1 -1
  73. package/dist/clis/codex/model.js +2 -2
  74. package/dist/clis/codex/screenshot.js +1 -1
  75. package/dist/clis/coupang/add-to-cart.js +3 -4
  76. package/dist/clis/coupang/search.js +2 -4
  77. package/dist/clis/coupang/utils.test.d.ts +1 -0
  78. package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
  79. package/dist/clis/ctrip/search.js +1 -1
  80. package/dist/clis/cursor/export.js +1 -1
  81. package/dist/clis/cursor/model.js +2 -2
  82. package/dist/clis/cursor/screenshot.js +1 -1
  83. package/dist/clis/jike/comment.js +2 -3
  84. package/dist/clis/jike/create.js +1 -2
  85. package/dist/clis/jike/feed.js +0 -1
  86. package/dist/clis/jike/like.js +1 -2
  87. package/dist/clis/jike/notifications.js +0 -1
  88. package/dist/clis/jike/post.yaml +1 -0
  89. package/dist/clis/jike/repost.js +1 -2
  90. package/dist/clis/jike/search.js +2 -3
  91. package/dist/clis/jike/topic.yaml +1 -0
  92. package/dist/clis/jike/user.yaml +1 -0
  93. package/dist/clis/jimeng/history.yaml +0 -1
  94. package/dist/clis/linkedin/search.js +7 -7
  95. package/dist/clis/linux-do/category.yaml +1 -0
  96. package/dist/clis/linux-do/search.yaml +4 -3
  97. package/dist/clis/linux-do/topic.yaml +1 -0
  98. package/dist/clis/notion/export.js +1 -1
  99. package/dist/clis/reddit/comment.js +3 -4
  100. package/dist/clis/reddit/read.js +4 -5
  101. package/dist/clis/reddit/save.js +2 -3
  102. package/dist/clis/reddit/saved.js +0 -1
  103. package/dist/clis/reddit/search.yaml +1 -0
  104. package/dist/clis/reddit/subscribe.js +0 -1
  105. package/dist/clis/reddit/upvote.js +2 -3
  106. package/dist/clis/reddit/upvoted.js +0 -1
  107. package/dist/clis/reddit/user-comments.yaml +1 -0
  108. package/dist/clis/reddit/user-posts.yaml +1 -0
  109. package/dist/clis/reddit/user.yaml +1 -0
  110. package/dist/clis/reuters/search.js +1 -1
  111. package/dist/clis/smzdm/search.js +2 -3
  112. package/dist/clis/stackoverflow/search.yaml +1 -0
  113. package/dist/clis/steam/top-sellers.yaml +29 -0
  114. package/dist/clis/twitter/accept.js +2 -2
  115. package/dist/clis/twitter/article.js +2 -2
  116. package/dist/clis/twitter/block.d.ts +1 -0
  117. package/dist/clis/twitter/block.js +88 -0
  118. package/dist/clis/twitter/delete.js +1 -1
  119. package/dist/clis/twitter/hide-reply.d.ts +1 -0
  120. package/dist/clis/twitter/hide-reply.js +66 -0
  121. package/dist/clis/twitter/like.js +1 -1
  122. package/dist/clis/twitter/post.js +1 -1
  123. package/dist/clis/twitter/reply-dm.js +1 -1
  124. package/dist/clis/twitter/reply.js +2 -2
  125. package/dist/clis/twitter/search.js +1 -1
  126. package/dist/clis/twitter/thread.js +2 -2
  127. package/dist/clis/twitter/trending.d.ts +1 -0
  128. package/dist/clis/twitter/trending.js +91 -0
  129. package/dist/clis/twitter/unblock.d.ts +1 -0
  130. package/dist/clis/twitter/unblock.js +71 -0
  131. package/dist/clis/v2ex/topic.yaml +1 -0
  132. package/dist/clis/weibo/hot.js +0 -1
  133. package/dist/clis/weread/book.js +1 -1
  134. package/dist/clis/weread/highlights.js +1 -1
  135. package/dist/clis/weread/notes.js +1 -1
  136. package/dist/clis/weread/search.js +1 -1
  137. package/dist/clis/wikipedia/search.js +1 -1
  138. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +15 -0
  139. package/dist/clis/xiaohongshu/creator-note-detail.js +69 -5
  140. package/dist/clis/xiaohongshu/creator-note-detail.test.js +80 -33
  141. package/dist/clis/xiaohongshu/creator-notes.js +35 -5
  142. package/dist/clis/xiaohongshu/creator-notes.test.js +35 -6
  143. package/dist/clis/xiaohongshu/creator-profile.js +0 -1
  144. package/dist/clis/xiaohongshu/creator-stats.js +0 -1
  145. package/dist/clis/xiaohongshu/download.js +2 -3
  146. package/dist/clis/xiaohongshu/feed.yaml +0 -1
  147. package/dist/clis/xiaohongshu/notifications.yaml +0 -1
  148. package/dist/clis/xiaohongshu/search.js +2 -2
  149. package/dist/clis/xiaohongshu/user.js +1 -2
  150. package/dist/clis/yahoo-finance/quote.js +0 -1
  151. package/dist/clis/youtube/search.js +1 -1
  152. package/dist/clis/youtube/transcript.js +1 -1
  153. package/dist/clis/youtube/video.js +1 -1
  154. package/dist/clis/zhihu/download.js +1 -2
  155. package/dist/clis/zhihu/question.js +1 -1
  156. package/dist/clis/zhihu/search.yaml +4 -3
  157. package/dist/commanderAdapter.d.ts +21 -0
  158. package/dist/commanderAdapter.js +111 -0
  159. package/dist/{engine.d.ts → discovery.d.ts} +0 -6
  160. package/dist/{engine.js → discovery.js} +1 -98
  161. package/dist/download/index.d.ts +2 -6
  162. package/dist/download/index.js +19 -46
  163. package/dist/engine.test.d.ts +1 -1
  164. package/dist/engine.test.js +8 -7
  165. package/dist/execution.d.ts +22 -0
  166. package/dist/execution.js +129 -0
  167. package/dist/explore.js +121 -107
  168. package/dist/external-clis.yaml +48 -0
  169. package/dist/external.d.ts +7 -2
  170. package/dist/external.js +11 -14
  171. package/dist/main.js +1 -1
  172. package/dist/pipeline/steps/browser.js +8 -2
  173. package/dist/registry.d.ts +2 -0
  174. package/dist/registry.js +2 -0
  175. package/dist/runtime.d.ts +5 -0
  176. package/dist/runtime.js +8 -0
  177. package/dist/serialization.d.ts +34 -0
  178. package/dist/serialization.js +63 -0
  179. package/dist/types.d.ts +4 -1
  180. package/docs/.vitepress/config.mts +14 -3
  181. package/docs/adapters/browser/arxiv.md +27 -0
  182. package/docs/adapters/browser/barchart.md +32 -0
  183. package/docs/adapters/browser/bloomberg.md +70 -0
  184. package/docs/adapters/browser/chaoxing.md +39 -0
  185. package/docs/adapters/browser/grok.md +35 -0
  186. package/docs/adapters/browser/hf.md +42 -0
  187. package/docs/adapters/browser/jike.md +45 -0
  188. package/docs/adapters/browser/jimeng.md +39 -0
  189. package/docs/adapters/browser/linux-do.md +45 -0
  190. package/docs/adapters/browser/sinafinance.md +35 -0
  191. package/docs/adapters/browser/stackoverflow.md +35 -0
  192. package/docs/adapters/browser/steam.md +26 -0
  193. package/docs/adapters/browser/twitter.md +3 -0
  194. package/docs/adapters/browser/weread.md +48 -0
  195. package/docs/adapters/browser/wikipedia.md +30 -0
  196. package/docs/adapters/browser/xiaohongshu.md +5 -1
  197. package/docs/adapters/desktop/chatgpt.md +3 -3
  198. package/docs/adapters/index.md +13 -0
  199. package/docs/advanced/download.md +4 -4
  200. package/docs/developer/architecture.md +17 -4
  201. package/package.json +1 -1
  202. package/scripts/check-doc-coverage.sh +69 -0
  203. package/scripts/copy-yaml.cjs +7 -0
  204. package/src/browser/cdp.ts +6 -1
  205. package/src/browser/page.ts +7 -1
  206. package/src/build-manifest.ts +25 -19
  207. package/src/cli.ts +218 -139
  208. package/src/clis/apple-podcasts/commands.test.ts +95 -0
  209. package/src/clis/apple-podcasts/search.ts +2 -2
  210. package/src/clis/apple-podcasts/top.ts +12 -2
  211. package/src/clis/arxiv/search.ts +1 -1
  212. package/src/clis/bilibili/dynamic.ts +1 -1
  213. package/src/clis/bilibili/favorite.ts +1 -1
  214. package/src/clis/bilibili/feed.ts +1 -1
  215. package/src/clis/bilibili/following.ts +1 -1
  216. package/src/clis/bilibili/history.ts +1 -1
  217. package/src/clis/bilibili/me.ts +1 -1
  218. package/src/clis/bilibili/ranking.ts +1 -1
  219. package/src/clis/bilibili/search.ts +3 -3
  220. package/src/clis/bilibili/subtitle.ts +1 -1
  221. package/src/clis/bilibili/user-videos.ts +1 -1
  222. package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
  223. package/src/clis/bloomberg/businessweek.ts +18 -0
  224. package/src/clis/bloomberg/economics.ts +18 -0
  225. package/src/clis/bloomberg/feeds.ts +16 -0
  226. package/src/clis/bloomberg/industries.ts +18 -0
  227. package/src/clis/bloomberg/main.ts +18 -0
  228. package/src/clis/bloomberg/markets.ts +18 -0
  229. package/src/clis/bloomberg/news.ts +136 -0
  230. package/src/clis/bloomberg/opinions.ts +18 -0
  231. package/src/clis/bloomberg/politics.ts +18 -0
  232. package/src/clis/bloomberg/tech.ts +18 -0
  233. package/src/clis/bloomberg/utils.test.ts +135 -0
  234. package/src/clis/bloomberg/utils.ts +429 -0
  235. package/src/clis/boss/batchgreet.ts +2 -2
  236. package/src/clis/boss/chatlist.ts +2 -2
  237. package/src/clis/boss/detail.ts +2 -2
  238. package/src/clis/boss/greet.ts +4 -4
  239. package/src/clis/boss/search.ts +1 -1
  240. package/src/clis/boss/send.ts +1 -1
  241. package/src/clis/boss/stats.ts +2 -2
  242. package/src/clis/chaoxing/assignments.ts +1 -1
  243. package/src/clis/chaoxing/exams.ts +1 -1
  244. package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
  245. package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
  246. package/src/clis/chatgpt/README.zh-CN.md +3 -3
  247. package/src/clis/chatgpt/read.ts +1 -1
  248. package/src/clis/chatwise/export.ts +1 -1
  249. package/src/clis/chatwise/model.ts +2 -2
  250. package/src/clis/chatwise/screenshot.ts +1 -1
  251. package/src/clis/codex/export.ts +1 -1
  252. package/src/clis/codex/model.ts +2 -2
  253. package/src/clis/codex/screenshot.ts +1 -1
  254. package/src/clis/coupang/add-to-cart.ts +3 -4
  255. package/src/clis/coupang/search.ts +2 -4
  256. package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
  257. package/src/clis/ctrip/search.ts +1 -1
  258. package/src/clis/cursor/export.ts +1 -1
  259. package/src/clis/cursor/model.ts +2 -2
  260. package/src/clis/cursor/screenshot.ts +1 -1
  261. package/src/clis/jike/comment.ts +2 -3
  262. package/src/clis/jike/create.ts +1 -2
  263. package/src/clis/jike/feed.ts +0 -1
  264. package/src/clis/jike/like.ts +1 -2
  265. package/src/clis/jike/notifications.ts +0 -1
  266. package/src/clis/jike/post.yaml +1 -0
  267. package/src/clis/jike/repost.ts +1 -2
  268. package/src/clis/jike/search.ts +2 -3
  269. package/src/clis/jike/topic.yaml +1 -0
  270. package/src/clis/jike/user.yaml +1 -0
  271. package/src/clis/jimeng/history.yaml +0 -1
  272. package/src/clis/linkedin/search.ts +7 -7
  273. package/src/clis/linux-do/category.yaml +1 -0
  274. package/src/clis/linux-do/search.yaml +4 -3
  275. package/src/clis/linux-do/topic.yaml +1 -0
  276. package/src/clis/notion/export.ts +1 -1
  277. package/src/clis/reddit/comment.ts +3 -4
  278. package/src/clis/reddit/read.ts +4 -5
  279. package/src/clis/reddit/save.ts +2 -3
  280. package/src/clis/reddit/saved.ts +0 -1
  281. package/src/clis/reddit/search.yaml +1 -0
  282. package/src/clis/reddit/subscribe.ts +0 -1
  283. package/src/clis/reddit/upvote.ts +2 -3
  284. package/src/clis/reddit/upvoted.ts +0 -1
  285. package/src/clis/reddit/user-comments.yaml +1 -0
  286. package/src/clis/reddit/user-posts.yaml +1 -0
  287. package/src/clis/reddit/user.yaml +1 -0
  288. package/src/clis/reuters/search.ts +1 -1
  289. package/src/clis/smzdm/search.ts +2 -3
  290. package/src/clis/stackoverflow/search.yaml +1 -0
  291. package/src/clis/steam/top-sellers.yaml +29 -0
  292. package/src/clis/twitter/accept.ts +2 -2
  293. package/src/clis/twitter/article.ts +2 -2
  294. package/src/clis/twitter/block.ts +92 -0
  295. package/src/clis/twitter/delete.ts +1 -1
  296. package/src/clis/twitter/hide-reply.ts +70 -0
  297. package/src/clis/twitter/like.ts +1 -1
  298. package/src/clis/twitter/post.ts +1 -1
  299. package/src/clis/twitter/reply-dm.ts +1 -1
  300. package/src/clis/twitter/reply.ts +2 -2
  301. package/src/clis/twitter/search.ts +1 -1
  302. package/src/clis/twitter/thread.ts +2 -2
  303. package/src/clis/twitter/trending.ts +113 -0
  304. package/src/clis/twitter/unblock.ts +75 -0
  305. package/src/clis/v2ex/topic.yaml +1 -0
  306. package/src/clis/weibo/hot.ts +0 -1
  307. package/src/clis/weread/book.ts +1 -1
  308. package/src/clis/weread/highlights.ts +1 -1
  309. package/src/clis/weread/notes.ts +1 -1
  310. package/src/clis/weread/search.ts +1 -1
  311. package/src/clis/wikipedia/search.ts +1 -1
  312. package/src/clis/xiaohongshu/creator-note-detail.test.ts +82 -33
  313. package/src/clis/xiaohongshu/creator-note-detail.ts +89 -5
  314. package/src/clis/xiaohongshu/creator-notes.test.ts +39 -6
  315. package/src/clis/xiaohongshu/creator-notes.ts +44 -5
  316. package/src/clis/xiaohongshu/creator-profile.ts +0 -1
  317. package/src/clis/xiaohongshu/creator-stats.ts +0 -1
  318. package/src/clis/xiaohongshu/download.ts +2 -3
  319. package/src/clis/xiaohongshu/feed.yaml +0 -1
  320. package/src/clis/xiaohongshu/notifications.yaml +0 -1
  321. package/src/clis/xiaohongshu/search.ts +2 -2
  322. package/src/clis/xiaohongshu/user.ts +1 -2
  323. package/src/clis/yahoo-finance/quote.ts +0 -1
  324. package/src/clis/youtube/search.ts +1 -1
  325. package/src/clis/youtube/transcript.ts +1 -1
  326. package/src/clis/youtube/video.ts +1 -1
  327. package/src/clis/zhihu/download.ts +1 -2
  328. package/src/clis/zhihu/question.ts +1 -1
  329. package/src/clis/zhihu/search.yaml +4 -3
  330. package/src/commanderAdapter.ts +113 -0
  331. package/src/{engine.ts → discovery.ts} +1 -108
  332. package/src/download/index.ts +21 -54
  333. package/src/engine.test.ts +8 -7
  334. package/src/execution.ts +138 -0
  335. package/src/explore.ts +135 -109
  336. package/src/external-clis.yaml +9 -0
  337. package/src/external.ts +15 -12
  338. package/src/main.ts +1 -1
  339. package/src/pipeline/steps/browser.ts +7 -2
  340. package/src/registry.ts +5 -0
  341. package/src/runtime.ts +9 -0
  342. package/src/serialization.ts +79 -0
  343. package/src/types.ts +1 -1
  344. package/tests/e2e/browser-public.test.ts +25 -0
  345. package/tests/e2e/public-commands.test.ts +55 -1
  346. package/dist/clis/twitter/trending.yaml +0 -46
  347. package/docs/public/CNAME +0 -1
  348. package/src/clis/twitter/trending.yaml +0 -46
  349. /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
  350. /package/dist/{chaoxing.test.d.ts → clis/bloomberg/businessweek.d.ts} +0 -0
  351. /package/dist/{coupang.test.d.ts → clis/bloomberg/economics.d.ts} +0 -0
  352. /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
  353. /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
  354. /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
@@ -8,12 +8,12 @@ cli({
8
8
  strategy: Strategy.COOKIE,
9
9
  browser: true,
10
10
  args: [
11
- { name: 'tweet_id', type: 'string', positional: true, required: true, help: 'Tweet ID or URL containing the article' },
11
+ { name: 'tweet-id', type: 'string', positional: true, required: true, help: 'Tweet ID or URL containing the article' },
12
12
  ],
13
13
  columns: ['title', 'author', 'content', 'url'],
14
14
  func: async (page, kwargs) => {
15
15
  // Extract tweet ID from URL if needed
16
- let tweetId = kwargs.tweet_id;
16
+ let tweetId = kwargs['tweet-id'];
17
17
  const urlMatch = tweetId.match(/\/(?:status|article)\/(\d+)/);
18
18
  if (urlMatch) tweetId = urlMatch[1];
19
19
 
@@ -0,0 +1,92 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ cli({
5
+ site: 'twitter',
6
+ name: 'block',
7
+ description: 'Block a Twitter user',
8
+ domain: 'x.com',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'username', type: 'string', positional: true, required: true, help: 'Twitter screen name (without @)' },
13
+ ],
14
+ columns: ['status', 'message'],
15
+ func: async (page: IPage | null, kwargs: any) => {
16
+ if (!page) throw new Error('Requires browser');
17
+ const username = kwargs.username.replace(/^@/, '');
18
+
19
+ await page.goto(`https://x.com/${username}`);
20
+ await page.wait(5);
21
+
22
+ const result = await page.evaluate(`(async () => {
23
+ try {
24
+ let attempts = 0;
25
+
26
+ // Check if already blocked (profile shows "Blocked" / unblock button)
27
+ while (attempts < 20) {
28
+ const blockedIndicator = document.querySelector('[data-testid$="-unblock"]');
29
+ if (blockedIndicator) {
30
+ return { ok: true, message: 'Already blocking @${username}.' };
31
+ }
32
+
33
+ const moreBtn = document.querySelector('[data-testid="userActions"]');
34
+ if (moreBtn) break;
35
+
36
+ await new Promise(r => setTimeout(r, 500));
37
+ attempts++;
38
+ }
39
+
40
+ const moreBtn = document.querySelector('[data-testid="userActions"]');
41
+ if (!moreBtn) {
42
+ return { ok: false, message: 'Could not find user actions menu. Are you logged in?' };
43
+ }
44
+
45
+ // Open the more actions menu
46
+ moreBtn.click();
47
+ await new Promise(r => setTimeout(r, 1000));
48
+
49
+ // Find the Block menu item
50
+ const menuItems = document.querySelectorAll('[role="menuitem"]');
51
+ let blockItem = null;
52
+ for (const item of menuItems) {
53
+ if (item.textContent && item.textContent.includes('Block')) {
54
+ blockItem = item;
55
+ break;
56
+ }
57
+ }
58
+
59
+ if (!blockItem) {
60
+ return { ok: false, message: 'Could not find Block option in menu.' };
61
+ }
62
+
63
+ blockItem.click();
64
+ await new Promise(r => setTimeout(r, 1000));
65
+
66
+ // Confirm the block in the dialog
67
+ const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
68
+ if (confirmBtn) {
69
+ confirmBtn.click();
70
+ await new Promise(r => setTimeout(r, 1500));
71
+ }
72
+
73
+ // Verify
74
+ const verify = document.querySelector('[data-testid$="-unblock"]');
75
+ if (verify) {
76
+ return { ok: true, message: 'Successfully blocked @${username}.' };
77
+ } else {
78
+ return { ok: false, message: 'Block action initiated but UI did not update.' };
79
+ }
80
+ } catch (e) {
81
+ return { ok: false, message: e.toString() };
82
+ }
83
+ })()`);
84
+
85
+ if (result.ok) await page.wait(2);
86
+
87
+ return [{
88
+ status: result.ok ? 'success' : 'failed',
89
+ message: result.message
90
+ }];
91
+ }
92
+ });
@@ -9,7 +9,7 @@ cli({
9
9
  strategy: Strategy.UI, // Utilizes internal DOM flows for interaction
10
10
  browser: true,
11
11
  args: [
12
- { name: 'url', type: 'string', required: true, help: 'The URL of the tweet to delete' },
12
+ { name: 'url', type: 'string', required: true, positional: true, help: 'The URL of the tweet to delete' },
13
13
  ],
14
14
  columns: ['status', 'message'],
15
15
  func: async (page: IPage | null, kwargs: any) => {
@@ -0,0 +1,70 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ cli({
5
+ site: 'twitter',
6
+ name: 'hide-reply',
7
+ description: 'Hide a reply on your tweet (useful for hiding bot/spam replies)',
8
+ domain: 'x.com',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'url', type: 'string', required: true, positional: true, help: 'The URL of the reply tweet to hide' },
13
+ ],
14
+ columns: ['status', 'message'],
15
+ func: async (page: IPage | null, kwargs: any) => {
16
+ if (!page) throw new Error('Requires browser');
17
+
18
+ await page.goto(kwargs.url);
19
+ await page.wait(5);
20
+
21
+ const result = await page.evaluate(`(async () => {
22
+ try {
23
+ let attempts = 0;
24
+ let moreMenu = null;
25
+
26
+ while (attempts < 20) {
27
+ moreMenu = document.querySelector('[aria-label="More"]');
28
+ if (moreMenu) break;
29
+ await new Promise(r => setTimeout(r, 500));
30
+ attempts++;
31
+ }
32
+
33
+ if (!moreMenu) {
34
+ return { ok: false, message: 'Could not find the "More" menu on this tweet. Are you logged in?' };
35
+ }
36
+
37
+ moreMenu.click();
38
+ await new Promise(r => setTimeout(r, 1000));
39
+
40
+ // Look for the "Hide reply" menu item
41
+ const items = document.querySelectorAll('[role="menuitem"]');
42
+ let hideItem = null;
43
+ for (const item of items) {
44
+ if (item.textContent && item.textContent.includes('Hide reply')) {
45
+ hideItem = item;
46
+ break;
47
+ }
48
+ }
49
+
50
+ if (!hideItem) {
51
+ return { ok: false, message: 'Could not find "Hide reply" option. This may not be a reply on your tweet.' };
52
+ }
53
+
54
+ hideItem.click();
55
+ await new Promise(r => setTimeout(r, 1500));
56
+
57
+ return { ok: true, message: 'Reply successfully hidden.' };
58
+ } catch (e) {
59
+ return { ok: false, message: e.toString() };
60
+ }
61
+ })()`);
62
+
63
+ if (result.ok) await page.wait(2);
64
+
65
+ return [{
66
+ status: result.ok ? 'success' : 'failed',
67
+ message: result.message
68
+ }];
69
+ }
70
+ });
@@ -9,7 +9,7 @@ cli({
9
9
  strategy: Strategy.UI, // Utilizes internal DOM flows for interaction
10
10
  browser: true,
11
11
  args: [
12
- { name: 'url', type: 'string', required: true, help: 'The URL of the tweet to like' },
12
+ { name: 'url', type: 'string', required: true, positional: true, help: 'The URL of the tweet to like' },
13
13
  ],
14
14
  columns: ['status', 'message'],
15
15
  func: async (page: IPage | null, kwargs: any) => {
@@ -9,7 +9,7 @@ cli({
9
9
  strategy: Strategy.UI,
10
10
  browser: true,
11
11
  args: [
12
- { name: 'text', type: 'string', required: true, help: 'The text content of the tweet' },
12
+ { name: 'text', type: 'string', required: true, positional: true, help: 'The text content of the tweet' },
13
13
  ],
14
14
  columns: ['status', 'message', 'text'],
15
15
  func: async (page: IPage | null, kwargs: any) => {
@@ -10,7 +10,7 @@ cli({
10
10
  browser: true,
11
11
  timeoutSeconds: 600, // 10 min — batch operation
12
12
  args: [
13
- { name: 'text', type: 'string', required: true, help: 'Message text to send (e.g. "我的微信 wxkabi")' },
13
+ { name: 'text', type: 'string', required: true, positional: true, help: 'Message text to send (e.g. "我的微信 wxkabi")' },
14
14
  { name: 'max', type: 'int', required: false, default: 20, help: 'Maximum number of conversations to reply to (default: 20)' },
15
15
  { name: 'skip-replied', type: 'boolean', required: false, default: true, help: 'Skip conversations where you already sent the same text (default: true)' },
16
16
  ],
@@ -9,8 +9,8 @@ cli({
9
9
  strategy: Strategy.UI, // Uses the UI directly to input and click post
10
10
  browser: true,
11
11
  args: [
12
- { name: 'url', type: 'string', required: true, help: 'The URL of the tweet to reply to' },
13
- { name: 'text', type: 'string', required: true, help: 'The text content of your reply' },
12
+ { name: 'url', type: 'string', required: true, positional: true, help: 'The URL of the tweet to reply to' },
13
+ { name: 'text', type: 'string', required: true, positional: true, help: 'The text content of your reply' },
14
14
  ],
15
15
  columns: ['status', 'message', 'text'],
16
16
  func: async (page: IPage | null, kwargs: any) => {
@@ -8,7 +8,7 @@ cli({
8
8
  strategy: Strategy.INTERCEPT, // Use intercept strategy
9
9
  browser: true,
10
10
  args: [
11
- { name: 'query', type: 'string', required: true },
11
+ { name: 'query', type: 'string', required: true, positional: true },
12
12
  { name: 'limit', type: 'int', default: 15 },
13
13
  ],
14
14
  columns: ['id', 'author', 'text', 'likes', 'views', 'url'],
@@ -122,12 +122,12 @@ cli({
122
122
  strategy: Strategy.COOKIE,
123
123
  browser: true,
124
124
  args: [
125
- { name: 'tweet_id', type: 'string', required: true },
125
+ { name: 'tweet-id', type: 'string', required: true },
126
126
  { name: 'limit', type: 'int', default: 50 },
127
127
  ],
128
128
  columns: ['id', 'author', 'text', 'likes', 'retweets', 'url'],
129
129
  func: async (page, kwargs) => {
130
- let tweetId = kwargs.tweet_id;
130
+ let tweetId = kwargs['tweet-id'];
131
131
  const urlMatch = tweetId.match(/\/status\/(\d+)/);
132
132
  if (urlMatch) tweetId = urlMatch[1];
133
133
 
@@ -0,0 +1,113 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ // ── Twitter GraphQL constants ──────────────────────────────────────────
4
+
5
+ const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
6
+
7
+ // ── Types ──────────────────────────────────────────────────────────────
8
+
9
+ interface TrendItem {
10
+ rank: number;
11
+ topic: string;
12
+ tweets: string;
13
+ category: string;
14
+ }
15
+
16
+ // ── CLI definition ────────────────────────────────────────────────────
17
+
18
+ cli({
19
+ site: 'twitter',
20
+ name: 'trending',
21
+ description: 'Twitter/X trending topics',
22
+ domain: 'x.com',
23
+ strategy: Strategy.COOKIE,
24
+ browser: true,
25
+ args: [
26
+ { name: 'limit', type: 'int', default: 20, help: 'Number of trends to show' },
27
+ ],
28
+ columns: ['rank', 'topic', 'tweets', 'category'],
29
+ func: async (page, kwargs) => {
30
+ const limit = kwargs.limit || 20;
31
+
32
+ // Navigate to trending page
33
+ await page.goto('https://x.com/explore/tabs/trending');
34
+ await page.wait(3);
35
+
36
+ // Extract CSRF token to verify login
37
+ const ct0 = await page.evaluate(`(() => {
38
+ return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
39
+ })()`);
40
+ if (!ct0) throw new Error('Not logged into x.com (no ct0 cookie)');
41
+
42
+ // Try legacy guide.json API first (faster than DOM scraping)
43
+ let trends: TrendItem[] = [];
44
+
45
+ const apiData = await page.evaluate(`(async () => {
46
+ const ct0 = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || '';
47
+ const r = await fetch('/i/api/2/guide.json?include_page_configuration=true', {
48
+ credentials: 'include',
49
+ headers: {
50
+ 'x-twitter-active-user': 'yes',
51
+ 'x-csrf-token': ct0,
52
+ 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'
53
+ }
54
+ });
55
+ return r.ok ? await r.json() : null;
56
+ })()`);
57
+
58
+ if (apiData) {
59
+ const instructions = apiData?.timeline?.instructions || [];
60
+ const entries = instructions.flatMap((inst: any) => inst?.addEntries?.entries || inst?.entries || []);
61
+ const apiTrends = entries
62
+ .filter((e: any) => e.content?.timelineModule)
63
+ .flatMap((e: any) => e.content.timelineModule.items || [])
64
+ .map((t: any) => t?.item?.content?.trend)
65
+ .filter(Boolean);
66
+
67
+ trends = apiTrends.map((t: any, i: number) => ({
68
+ rank: i + 1,
69
+ topic: t.name,
70
+ tweets: t.tweetCount ? String(t.tweetCount) : 'N/A',
71
+ category: t.trendMetadata?.domainContext || '',
72
+ }));
73
+ }
74
+
75
+ // Fallback: scrape from the loaded DOM
76
+ if (trends.length === 0) {
77
+ await page.wait(2);
78
+ const domTrends = await page.evaluate(`(() => {
79
+ const items = [];
80
+ const cells = document.querySelectorAll('[data-testid="trend"]');
81
+ cells.forEach((cell) => {
82
+ const text = cell.textContent || '';
83
+ if (text.includes('Promoted')) return;
84
+ const container = cell.querySelector(':scope > div');
85
+ if (!container) return;
86
+ const divs = container.children;
87
+ // Structure: divs[0] = rank + category, divs[1] = topic name, divs[2] = extra
88
+ const topicEl = divs.length >= 2 ? divs[1] : null;
89
+ const topic = topicEl ? topicEl.textContent.trim() : '';
90
+ const catEl = divs.length >= 1 ? divs[0] : null;
91
+ const catText = catEl ? catEl.textContent.trim() : '';
92
+ const category = catText.replace(/^\\d+\\s*/, '').replace(/^\\xB7\\s*/, '').trim();
93
+ const extraEl = divs.length >= 3 ? divs[2] : null;
94
+ const extra = extraEl ? extraEl.textContent.trim() : '';
95
+ if (topic) {
96
+ items.push({ rank: items.length + 1, topic, tweets: extra || 'N/A', category });
97
+ }
98
+ });
99
+ return items;
100
+ })()`);
101
+
102
+ if (Array.isArray(domTrends) && domTrends.length > 0) {
103
+ trends = domTrends;
104
+ }
105
+ }
106
+
107
+ if (trends.length === 0) {
108
+ throw new Error('No trending data found. API may have changed or login may be required.');
109
+ }
110
+
111
+ return trends.slice(0, limit);
112
+ },
113
+ });
@@ -0,0 +1,75 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ cli({
5
+ site: 'twitter',
6
+ name: 'unblock',
7
+ description: 'Unblock a Twitter user',
8
+ domain: 'x.com',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'username', type: 'string', positional: true, required: true, help: 'Twitter screen name (without @)' },
13
+ ],
14
+ columns: ['status', 'message'],
15
+ func: async (page: IPage | null, kwargs: any) => {
16
+ if (!page) throw new Error('Requires browser');
17
+ const username = kwargs.username.replace(/^@/, '');
18
+
19
+ await page.goto(`https://x.com/${username}`);
20
+ await page.wait(5);
21
+
22
+ const result = await page.evaluate(`(async () => {
23
+ try {
24
+ let attempts = 0;
25
+ let unblockBtn = null;
26
+
27
+ while (attempts < 20) {
28
+ // Check if not blocked (follow button visible means not blocked)
29
+ const followBtn = document.querySelector('[data-testid$="-follow"]');
30
+ if (followBtn) {
31
+ return { ok: true, message: 'Not blocking @${username} (already unblocked).' };
32
+ }
33
+
34
+ unblockBtn = document.querySelector('[data-testid$="-unblock"]');
35
+ if (unblockBtn) break;
36
+
37
+ await new Promise(r => setTimeout(r, 500));
38
+ attempts++;
39
+ }
40
+
41
+ if (!unblockBtn) {
42
+ return { ok: false, message: 'Could not find Unblock button. Are you logged in?' };
43
+ }
44
+
45
+ // Click the unblock button — this opens a confirmation dialog
46
+ unblockBtn.click();
47
+ await new Promise(r => setTimeout(r, 1000));
48
+
49
+ // Confirm the unblock in the dialog
50
+ const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
51
+ if (confirmBtn) {
52
+ confirmBtn.click();
53
+ await new Promise(r => setTimeout(r, 1000));
54
+ }
55
+
56
+ // Verify
57
+ const verify = document.querySelector('[data-testid$="-follow"]');
58
+ if (verify) {
59
+ return { ok: true, message: 'Successfully unblocked @${username}.' };
60
+ } else {
61
+ return { ok: false, message: 'Unblock action initiated but UI did not update.' };
62
+ }
63
+ } catch (e) {
64
+ return { ok: false, message: e.toString() };
65
+ }
66
+ })()`);
67
+
68
+ if (result.ok) await page.wait(2);
69
+
70
+ return [{
71
+ status: result.ok ? 'success' : 'failed',
72
+ message: result.message
73
+ }];
74
+ }
75
+ });
@@ -7,6 +7,7 @@ browser: false
7
7
 
8
8
  args:
9
9
  id:
10
+ positional: true
10
11
  type: str
11
12
  required: true
12
13
  description: Topic ID
@@ -17,7 +17,6 @@ cli({
17
17
  func: async (page, kwargs) => {
18
18
  const count = Math.min(kwargs.limit || 30, 50);
19
19
  await page.goto('https://weibo.com');
20
- await page.wait(2);
21
20
  const data = await page.evaluate(`
22
21
  (async () => {
23
22
  const resp = await fetch('/ajax/statuses/hot_band', {credentials: 'include'});
@@ -9,7 +9,7 @@ cli({
9
9
  domain: 'weread.qq.com',
10
10
  strategy: Strategy.COOKIE,
11
11
  args: [
12
- { name: 'bookId', positional: true, required: true, help: 'Book ID (numeric, from search or shelf results)' },
12
+ { name: 'book-id', positional: true, required: true, help: 'Book ID (numeric, from search or shelf results)' },
13
13
  ],
14
14
  columns: ['title', 'author', 'publisher', 'intro', 'category', 'rating'],
15
15
  func: async (page: IPage, args) => {
@@ -9,7 +9,7 @@ cli({
9
9
  domain: 'weread.qq.com',
10
10
  strategy: Strategy.COOKIE,
11
11
  args: [
12
- { name: 'bookId', positional: true, required: true, help: 'Book ID (from shelf or search results)' },
12
+ { name: 'book-id', positional: true, required: true, help: 'Book ID (from shelf or search results)' },
13
13
  { name: 'limit', type: 'int', default: 20, help: 'Max results' },
14
14
  ],
15
15
  columns: ['chapter', 'text', 'createTime'],
@@ -9,7 +9,7 @@ cli({
9
9
  domain: 'weread.qq.com',
10
10
  strategy: Strategy.COOKIE,
11
11
  args: [
12
- { name: 'bookId', positional: true, required: true, help: 'Book ID (from shelf or search results)' },
12
+ { name: 'book-id', positional: true, required: true, help: 'Book ID (from shelf or search results)' },
13
13
  { name: 'limit', type: 'int', default: 20, help: 'Max results' },
14
14
  ],
15
15
  columns: ['chapter', 'text', 'review', 'createTime'],
@@ -9,7 +9,7 @@ cli({
9
9
  strategy: Strategy.PUBLIC,
10
10
  browser: false,
11
11
  args: [
12
- { name: 'keyword', positional: true, required: true, help: 'Search keyword' },
12
+ { name: 'query', positional: true, required: true, help: 'Search keyword' },
13
13
  { name: 'limit', type: 'int', default: 10, help: 'Max results' },
14
14
  ],
15
15
  columns: ['rank', 'title', 'author', 'bookId'],
@@ -11,7 +11,7 @@ cli({
11
11
  strategy: Strategy.PUBLIC,
12
12
  browser: false,
13
13
  args: [
14
- { name: 'keyword', positional: true, required: true, help: 'Search keyword' },
14
+ { name: 'query', positional: true, required: true, help: 'Search keyword' },
15
15
  { name: 'limit', type: 'int', default: 10, help: 'Max results' },
16
16
  { name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' },
17
17
  ],
@@ -1,13 +1,19 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import type { IPage } from '../../types.js';
3
3
  import { getRegistry } from '../../registry.js';
4
- import { appendAudienceRows, appendTrendRows, parseCreatorNoteDetailText } from './creator-note-detail.js';
4
+ import { appendAudienceRows, appendTrendRows, parseCreatorNoteDetailDomData, parseCreatorNoteDetailText } from './creator-note-detail.js';
5
5
  import './creator-note-detail.js';
6
6
 
7
7
  function createPageMock(evaluateResult: any): IPage {
8
+ const evaluate = Array.isArray(evaluateResult)
9
+ ? vi.fn()
10
+ .mockResolvedValueOnce(evaluateResult[0])
11
+ .mockResolvedValue(evaluateResult[evaluateResult.length - 1])
12
+ : vi.fn().mockResolvedValue(evaluateResult);
13
+
8
14
  return {
9
15
  goto: vi.fn().mockResolvedValue(undefined),
10
- evaluate: vi.fn().mockResolvedValue(evaluateResult),
16
+ evaluate,
11
17
  snapshot: vi.fn().mockResolvedValue(undefined),
12
18
  click: vi.fn().mockResolvedValue(undefined),
13
19
  typeText: vi.fn().mockResolvedValue(undefined),
@@ -93,6 +99,47 @@ describe('xiaohongshu creator-note-detail', () => {
93
99
  ]);
94
100
  });
95
101
 
102
+ it('parses structured note detail dom data into rows', () => {
103
+ expect(parseCreatorNoteDetailDomData({
104
+ title: '神雕侠侣战力金字塔',
105
+ infoText: '神雕侠侣战力金字塔\n#武侠\n2025-12-04 19:45\n切换笔记',
106
+ sections: [
107
+ {
108
+ title: '基础数据',
109
+ metrics: [
110
+ { label: '曝光数', value: '898204', extra: '粉丝占比 0.5%' },
111
+ { label: '观看数', value: '148284', extra: '粉丝占比 0.6%' },
112
+ { label: '封面点击率', value: '17.1%', extra: '粉丝 19.1%' },
113
+ { label: '平均观看时长', value: '30.1秒', extra: '粉丝 17.7秒' },
114
+ { label: '涨粉数', value: '101', extra: '' },
115
+ ],
116
+ },
117
+ {
118
+ title: '互动数据',
119
+ metrics: [
120
+ { label: '点赞数', value: '2280', extra: '粉丝占比 3.6%' },
121
+ { label: '评论数', value: '319', extra: '粉丝占比 9.4%' },
122
+ { label: '收藏数', value: '466', extra: '粉丝占比 9.4%' },
123
+ { label: '分享数', value: '33', extra: '粉丝占比 17.7%' },
124
+ ],
125
+ },
126
+ ],
127
+ }, '693155fc000000000d03b42c')).toEqual([
128
+ { section: '笔记信息', metric: 'note_id', value: '693155fc000000000d03b42c', extra: '' },
129
+ { section: '笔记信息', metric: 'title', value: '神雕侠侣战力金字塔', extra: '' },
130
+ { section: '笔记信息', metric: 'published_at', value: '2025-12-04 19:45', extra: '' },
131
+ { section: '基础数据', metric: '曝光数', value: '898204', extra: '粉丝占比 0.5%' },
132
+ { section: '基础数据', metric: '观看数', value: '148284', extra: '粉丝占比 0.6%' },
133
+ { section: '基础数据', metric: '封面点击率', value: '17.1%', extra: '粉丝 19.1%' },
134
+ { section: '基础数据', metric: '平均观看时长', value: '30.1秒', extra: '粉丝 17.7秒' },
135
+ { section: '基础数据', metric: '涨粉数', value: '101', extra: '' },
136
+ { section: '互动数据', metric: '点赞数', value: '2280', extra: '粉丝占比 3.6%' },
137
+ { section: '互动数据', metric: '评论数', value: '319', extra: '粉丝占比 9.4%' },
138
+ { section: '互动数据', metric: '收藏数', value: '466', extra: '粉丝占比 9.4%' },
139
+ { section: '互动数据', metric: '分享数', value: '33', extra: '粉丝占比 17.7%' },
140
+ ]);
141
+ });
142
+
96
143
  it('appends audience source and portrait rows from API payloads', () => {
97
144
  const rows = appendAudienceRows([], {
98
145
  audienceSource: {
@@ -171,40 +218,42 @@ describe('xiaohongshu creator-note-detail', () => {
171
218
  const cmd = getRegistry().get('xiaohongshu/creator-note-detail');
172
219
  expect(cmd?.func).toBeTypeOf('function');
173
220
 
174
- const page = createPageMock(`笔记数据详情
175
- 示例笔记
176
- 2026-03-19 12:00
177
- 曝光数
178
- 100
179
- 粉丝占比 10%
180
- 观看数
181
- 50
182
- 粉丝占比 20%
183
- 封面点击率
184
- 12%
185
- 粉丝 11%
186
- 平均观看时长
187
- 30秒
188
- 粉丝 31秒
189
- 涨粉数
190
- 2
191
- 点赞数
192
- 8
193
- 粉丝占比 25%
194
- 评论数
195
- 1
196
- 粉丝占比 0%
197
- 收藏数
198
- 3
199
- 粉丝占比 50%
200
- 分享数
201
- 0
202
- 粉丝占比 0%`);
221
+ const page = createPageMock([
222
+ {
223
+ title: '示例笔记',
224
+ infoText: '示例笔记\n2026-03-19 12:00\n切换笔记',
225
+ sections: [
226
+ {
227
+ title: '基础数据',
228
+ metrics: [
229
+ { label: '曝光数', value: '100', extra: '粉丝占比 10%' },
230
+ { label: '观看数', value: '50', extra: '粉丝占比 20%' },
231
+ { label: '封面点击率', value: '12%', extra: '粉丝 11%' },
232
+ { label: '平均观看时长', value: '30秒', extra: '粉丝 31秒' },
233
+ { label: '涨粉数', value: '2', extra: '' },
234
+ ],
235
+ },
236
+ {
237
+ title: '互动数据',
238
+ metrics: [
239
+ { label: '点赞数', value: '8', extra: '粉丝占比 25%' },
240
+ { label: '评论数', value: '1', extra: '粉丝占比 0%' },
241
+ { label: '收藏数', value: '3', extra: '粉丝占比 50%' },
242
+ { label: '分享数', value: '0', extra: '粉丝占比 0%' },
243
+ ],
244
+ },
245
+ ],
246
+ },
247
+ null,
248
+ null,
249
+ null,
250
+ null,
251
+ ]);
203
252
 
204
- const result = await cmd!.func!(page, { note_id: 'demo-note-id' });
253
+ const result = await cmd!.func!(page, { 'note-id': 'demo-note-id' });
205
254
 
206
255
  expect((page.goto as any).mock.calls[0][0]).toBe('https://creator.xiaohongshu.com/statistics/note-detail?noteId=demo-note-id');
207
- expect((page.evaluate as any).mock.calls[0][0]).toBe('() => document.body.innerText');
256
+ expect((page.evaluate as any).mock.calls[0][0]).toContain("document.querySelector('.note-title')");
208
257
  expect(result).toEqual([
209
258
  { section: '笔记信息', metric: 'note_id', value: 'demo-note-id', extra: '' },
210
259
  { section: '笔记信息', metric: 'title', value: '示例笔记', extra: '' },