@jackwener/opencli 1.7.22 → 1.8.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 (346) hide show
  1. package/README.md +35 -194
  2. package/README.zh-CN.md +42 -260
  3. package/cli-manifest.json +8160 -4392
  4. package/clis/12306/me.js +73 -0
  5. package/clis/12306/orders.js +96 -0
  6. package/clis/12306/passengers.js +90 -0
  7. package/clis/12306/price.js +166 -0
  8. package/clis/12306/stations.js +66 -0
  9. package/clis/12306/train.js +91 -0
  10. package/clis/12306/trains.js +119 -0
  11. package/clis/12306/utils.js +272 -0
  12. package/clis/12306/utils.test.js +331 -0
  13. package/clis/36kr/article.js +6 -3
  14. package/clis/36kr/article.test.js +46 -0
  15. package/clis/_atlassian/shared.js +577 -0
  16. package/clis/_atlassian/shared.test.js +170 -0
  17. package/clis/apple-podcasts/commands.test.js +20 -0
  18. package/clis/apple-podcasts/search.js +2 -2
  19. package/clis/barchart/greeks.js +144 -56
  20. package/clis/barchart/greeks.test.js +138 -0
  21. package/clis/bilibili/comment.js +125 -0
  22. package/clis/bilibili/comment.test.js +153 -0
  23. package/clis/bilibili/comments.js +116 -21
  24. package/clis/bilibili/comments.test.js +77 -18
  25. package/clis/bilibili/subtitle.js +76 -31
  26. package/clis/bilibili/subtitle.test.js +156 -9
  27. package/clis/bilibili/summary.js +167 -0
  28. package/clis/bilibili/summary.test.js +210 -0
  29. package/clis/bilibili/utils.js +63 -5
  30. package/clis/bilibili/utils.test.js +45 -1
  31. package/clis/booking/booking.test.js +356 -0
  32. package/clis/booking/search.js +351 -0
  33. package/clis/chatgpt/envelope.test.js +108 -0
  34. package/clis/chatgpt/image.js +2 -2
  35. package/clis/chatgpt/image.test.js +6 -0
  36. package/clis/chatgpt/utils.js +148 -41
  37. package/clis/chatgpt/utils.test.js +92 -2
  38. package/clis/chess/analyze.js +35 -0
  39. package/clis/chess/analyze.test.js +79 -0
  40. package/clis/chess/game.js +114 -0
  41. package/clis/chess/game.test.js +178 -0
  42. package/clis/chess/games.js +67 -0
  43. package/clis/chess/games.test.js +164 -0
  44. package/clis/chess/stats.js +32 -0
  45. package/clis/chess/stats.test.js +79 -0
  46. package/clis/chess/utils.js +170 -0
  47. package/clis/chess/utils.test.js +230 -0
  48. package/clis/confluence/commands.test.js +195 -0
  49. package/clis/confluence/create.js +39 -0
  50. package/clis/confluence/page.js +23 -0
  51. package/clis/confluence/search.js +34 -0
  52. package/clis/confluence/shared.js +173 -0
  53. package/clis/confluence/update.js +38 -0
  54. package/clis/douyin/_shared/browser-fetch.js +44 -20
  55. package/clis/douyin/_shared/browser-fetch.test.js +22 -1
  56. package/clis/douyin/_shared/evaluate-result.js +16 -0
  57. package/clis/douyin/_shared/tos-upload.js +105 -69
  58. package/clis/douyin/_shared/vod-upload.js +212 -0
  59. package/clis/douyin/_shared/vod-upload.test.js +38 -0
  60. package/clis/douyin/delete.js +137 -4
  61. package/clis/douyin/delete.test.js +90 -1
  62. package/clis/douyin/hashtag.js +84 -23
  63. package/clis/douyin/hashtag.test.js +113 -0
  64. package/clis/douyin/publish-upload-id.test.js +170 -0
  65. package/clis/douyin/publish.js +88 -42
  66. package/clis/douyin/user-videos.js +9 -2
  67. package/clis/douyin/user-videos.test.js +43 -0
  68. package/clis/flomo/memos.js +228 -0
  69. package/clis/flomo/memos.test.js +144 -0
  70. package/clis/geogebra/add-circle.js +46 -0
  71. package/clis/geogebra/add-line.js +35 -0
  72. package/clis/geogebra/add-point.js +27 -0
  73. package/clis/geogebra/add-polygon.js +25 -0
  74. package/clis/geogebra/eval.js +35 -0
  75. package/clis/geogebra/geogebra.test.js +175 -0
  76. package/clis/geogebra/hexagon.js +62 -0
  77. package/clis/geogebra/info.js +72 -0
  78. package/clis/geogebra/list.js +35 -0
  79. package/clis/geogebra/triangle.js +60 -0
  80. package/clis/geogebra/utils.js +271 -0
  81. package/clis/gitee/search.js +2 -2
  82. package/clis/gitee/search.test.js +65 -0
  83. package/clis/jike/post.js +27 -17
  84. package/clis/jike/read.test.js +86 -0
  85. package/clis/jike/topic.js +32 -19
  86. package/clis/jike/user.js +33 -20
  87. package/clis/jira/attachments.js +28 -0
  88. package/clis/jira/commands.test.js +287 -0
  89. package/clis/jira/comments.js +28 -0
  90. package/clis/jira/issue.js +28 -0
  91. package/clis/jira/links.js +28 -0
  92. package/clis/jira/search.js +47 -0
  93. package/clis/jira/shared.js +256 -0
  94. package/clis/lesswrong/comments.js +1 -1
  95. package/clis/lesswrong/curated.js +1 -1
  96. package/clis/lesswrong/frontpage.js +1 -1
  97. package/clis/lesswrong/frontpage.test.js +37 -0
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/top-month.js +1 -1
  104. package/clis/lesswrong/top-week.js +1 -1
  105. package/clis/lesswrong/top-year.js +1 -1
  106. package/clis/lesswrong/top.js +1 -1
  107. package/clis/linkedin/connect.js +401 -0
  108. package/clis/linkedin/connect.test.js +213 -0
  109. package/clis/linkedin/inbox.js +234 -0
  110. package/clis/linkedin/inbox.test.js +152 -0
  111. package/clis/linkedin/job-detail.js +167 -0
  112. package/clis/linkedin/job-detail.test.js +38 -0
  113. package/clis/linkedin/jobs-preferences.js +113 -0
  114. package/clis/linkedin/jobs-preferences.test.js +43 -0
  115. package/clis/linkedin/people-search.js +262 -0
  116. package/clis/linkedin/people-search.test.js +216 -0
  117. package/clis/linkedin/post-analytics.js +74 -0
  118. package/clis/linkedin/post-analytics.test.js +40 -0
  119. package/clis/linkedin/posts-core.js +241 -0
  120. package/clis/linkedin/posts.js +22 -0
  121. package/clis/linkedin/posts.test.js +40 -0
  122. package/clis/linkedin/profile-analytics.js +104 -0
  123. package/clis/linkedin/profile-analytics.test.js +67 -0
  124. package/clis/linkedin/profile-experience.js +671 -0
  125. package/clis/linkedin/profile-experience.test.js +152 -0
  126. package/clis/linkedin/profile-projects.js +311 -0
  127. package/clis/linkedin/profile-projects.test.js +111 -0
  128. package/clis/linkedin/profile-read.js +148 -0
  129. package/clis/linkedin/profile-read.test.js +77 -0
  130. package/clis/linkedin/safe-send.js +357 -0
  131. package/clis/linkedin/safe-send.test.js +204 -0
  132. package/clis/linkedin/salesnav-inbox.js +210 -0
  133. package/clis/linkedin/salesnav-inbox.test.js +113 -0
  134. package/clis/linkedin/salesnav-message.js +360 -0
  135. package/clis/linkedin/salesnav-message.test.js +172 -0
  136. package/clis/linkedin/salesnav-search.js +186 -0
  137. package/clis/linkedin/salesnav-search.test.js +76 -0
  138. package/clis/linkedin/salesnav-thread.js +212 -0
  139. package/clis/linkedin/salesnav-thread.test.js +79 -0
  140. package/clis/linkedin/sent-invitations.js +92 -0
  141. package/clis/linkedin/sent-invitations.test.js +62 -0
  142. package/clis/linkedin/services-read.js +213 -0
  143. package/clis/linkedin/services-read.test.js +105 -0
  144. package/clis/linkedin/shared.js +124 -0
  145. package/clis/linkedin/thread-snapshot.js +214 -0
  146. package/clis/linkedin/thread-snapshot.test.js +89 -0
  147. package/clis/linkedin/timeline.js +14 -7
  148. package/clis/linkedin-learning/course.js +138 -0
  149. package/clis/linkedin-learning/course.test.js +114 -0
  150. package/clis/linkedin-learning/search.js +155 -0
  151. package/clis/linkedin-learning/search.test.js +144 -0
  152. package/clis/linkedin-learning/trending.js +133 -0
  153. package/clis/linkedin-learning/trending.test.js +123 -0
  154. package/clis/notebooklm/add-source.js +269 -0
  155. package/clis/notebooklm/add-source.test.js +97 -0
  156. package/clis/notebooklm/create.js +76 -0
  157. package/clis/notebooklm/create.test.js +58 -0
  158. package/clis/notebooklm/generate-audio.js +91 -0
  159. package/clis/notebooklm/generate-audio.test.js +63 -0
  160. package/clis/notebooklm/generate-slides.js +106 -0
  161. package/clis/notebooklm/generate-slides.test.js +75 -0
  162. package/clis/notebooklm/open.test.js +10 -10
  163. package/clis/notebooklm/rpc.js +20 -6
  164. package/clis/notebooklm/rpc.test.js +27 -1
  165. package/clis/notebooklm/utils.js +100 -24
  166. package/clis/notebooklm/utils.test.js +60 -1
  167. package/clis/notebooklm/write-note.js +103 -0
  168. package/clis/notebooklm/write-note.test.js +70 -0
  169. package/clis/pixiv/detail.js +41 -34
  170. package/clis/pixiv/detail.test.js +93 -0
  171. package/clis/pixiv/user.js +36 -31
  172. package/clis/pixiv/user.test.js +100 -0
  173. package/clis/pixiv/utils.js +56 -7
  174. package/clis/powerchina/search.js +3 -3
  175. package/clis/powerchina/search.test.js +27 -1
  176. package/clis/reddit/extract-media.test.js +149 -0
  177. package/clis/reddit/frontpage.js +47 -9
  178. package/clis/reddit/frontpage.test.js +34 -0
  179. package/clis/reddit/home.js +31 -1
  180. package/clis/reddit/home.test.js +46 -3
  181. package/clis/reddit/hot.js +32 -1
  182. package/clis/reddit/hot.test.js +15 -1
  183. package/clis/reddit/popular.js +39 -1
  184. package/clis/reddit/popular.test.js +26 -0
  185. package/clis/reddit/saved.js +1 -1
  186. package/clis/reddit/search.js +38 -1
  187. package/clis/reddit/search.test.js +26 -0
  188. package/clis/reddit/subreddit.js +52 -7
  189. package/clis/reddit/subreddit.test.js +31 -0
  190. package/clis/reddit/subscribed.js +165 -0
  191. package/clis/reddit/subscribed.test.js +168 -0
  192. package/clis/reddit/upvoted.js +1 -1
  193. package/clis/suno/commands.test.js +188 -0
  194. package/clis/suno/download.js +140 -0
  195. package/clis/suno/download.test.js +151 -0
  196. package/clis/suno/generate.js +231 -0
  197. package/clis/suno/generate.test.js +252 -0
  198. package/clis/suno/list.js +79 -0
  199. package/clis/suno/status.js +63 -0
  200. package/clis/suno/utils.js +549 -0
  201. package/clis/suno/utils.test.js +329 -0
  202. package/clis/twitter/device-follow.js +193 -0
  203. package/clis/twitter/device-follow.test.js +287 -0
  204. package/clis/twitter/download.js +443 -73
  205. package/clis/twitter/download.test.js +457 -0
  206. package/clis/twitter/followers.js +6 -2
  207. package/clis/twitter/followers.test.js +19 -1
  208. package/clis/twitter/following.js +14 -5
  209. package/clis/twitter/following.test.js +29 -0
  210. package/clis/twitter/likes.js +12 -4
  211. package/clis/twitter/likes.test.js +26 -1
  212. package/clis/twitter/list-add.js +1 -1
  213. package/clis/twitter/list-create.js +155 -0
  214. package/clis/twitter/list-create.test.js +169 -0
  215. package/clis/twitter/list-remove.js +13 -6
  216. package/clis/twitter/list-remove.test.js +74 -0
  217. package/clis/twitter/list-tweets.js +6 -2
  218. package/clis/twitter/list-tweets.test.js +41 -1
  219. package/clis/twitter/lists.js +31 -4
  220. package/clis/twitter/lists.test.js +152 -16
  221. package/clis/twitter/notifications.js +4 -4
  222. package/clis/twitter/post.js +62 -4
  223. package/clis/twitter/post.test.js +35 -3
  224. package/clis/twitter/profile.js +81 -28
  225. package/clis/twitter/profile.test.js +113 -2
  226. package/clis/twitter/quote.js +9 -4
  227. package/clis/twitter/reply.js +13 -10
  228. package/clis/twitter/reply.test.js +41 -0
  229. package/clis/twitter/search.js +7 -3
  230. package/clis/twitter/search.test.js +41 -0
  231. package/clis/twitter/shared.js +155 -0
  232. package/clis/twitter/shared.test.js +465 -1
  233. package/clis/twitter/thread.js +10 -2
  234. package/clis/twitter/thread.test.js +58 -0
  235. package/clis/twitter/timeline.js +6 -2
  236. package/clis/twitter/timeline.test.js +2 -0
  237. package/clis/twitter/tweets.js +3 -2
  238. package/clis/twitter/tweets.test.js +1 -1
  239. package/clis/twitter/utils.js +53 -16
  240. package/clis/upwork/detail.js +132 -0
  241. package/clis/upwork/feed.js +109 -0
  242. package/clis/upwork/search.js +115 -0
  243. package/clis/upwork/upwork.test.js +566 -0
  244. package/clis/upwork/utils.js +323 -0
  245. package/clis/weibo/delete.js +172 -0
  246. package/clis/weibo/delete.test.js +94 -0
  247. package/clis/weibo/publish.js +37 -14
  248. package/clis/weibo/publish.test.js +14 -5
  249. package/clis/weibo/user-posts.js +234 -0
  250. package/clis/weibo/user-posts.test.js +92 -0
  251. package/clis/weread/book-search.js +438 -0
  252. package/clis/weread/book-search.test.js +242 -0
  253. package/clis/weread/search-regression.test.js +98 -11
  254. package/clis/weread/search.js +32 -9
  255. package/clis/weread-official/book.js +135 -0
  256. package/clis/weread-official/commands.test.js +385 -0
  257. package/clis/weread-official/discover.js +107 -0
  258. package/clis/weread-official/list-apis.js +95 -0
  259. package/clis/weread-official/notes.js +171 -0
  260. package/clis/weread-official/readdata.js +158 -0
  261. package/clis/weread-official/review.js +93 -0
  262. package/clis/weread-official/search.js +106 -0
  263. package/clis/weread-official/shelf.js +97 -0
  264. package/clis/weread-official/utils.js +293 -0
  265. package/clis/weread-official/utils.test.js +242 -0
  266. package/clis/wikipedia/trending.js +7 -3
  267. package/clis/wikipedia/trending.test.js +57 -0
  268. package/clis/xianyu/chat.js +24 -109
  269. package/clis/xianyu/chat.test.js +5 -0
  270. package/clis/xianyu/im.js +322 -0
  271. package/clis/xianyu/im.test.js +253 -0
  272. package/clis/xianyu/inbox.js +96 -0
  273. package/clis/xianyu/messages.js +91 -0
  274. package/clis/xianyu/reply.js +82 -0
  275. package/clis/xiaohongshu/creator-note-detail.js +166 -28
  276. package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
  277. package/clis/xiaohongshu/creator-notes-summary.js +2 -1
  278. package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
  279. package/clis/xiaohongshu/creator-notes.js +252 -2
  280. package/clis/xiaohongshu/creator-notes.test.js +90 -1
  281. package/clis/xiaohongshu/creator-stats.js +2 -1
  282. package/clis/xiaohongshu/creator-stats.test.js +24 -0
  283. package/clis/xiaohongshu/delete-note.js +260 -0
  284. package/clis/xiaohongshu/delete-note.test.js +172 -0
  285. package/clis/xiaohongshu/download.js +97 -39
  286. package/clis/xiaohongshu/download.test.js +201 -0
  287. package/clis/xiaohongshu/publish.js +48 -8
  288. package/clis/xiaohongshu/publish.test.js +65 -10
  289. package/clis/xiaohongshu/user-helpers.test.js +41 -0
  290. package/clis/xiaohongshu/user.js +27 -4
  291. package/clis/xiaoyuzhou/download.js +1 -1
  292. package/clis/xiaoyuzhou/transcript.js +1 -1
  293. package/clis/youdao/note.js +258 -0
  294. package/clis/youdao/note.test.js +99 -0
  295. package/clis/youtube/transcript.js +397 -24
  296. package/clis/youtube/transcript.test.js +196 -6
  297. package/clis/zhihu/answer-comments.js +280 -0
  298. package/clis/zhihu/answer-comments.test.js +287 -0
  299. package/clis/zhihu/answer-detail.js +2 -19
  300. package/clis/zhihu/answer-detail.test.js +8 -0
  301. package/clis/zhihu/collection.js +17 -16
  302. package/clis/zhihu/collection.test.js +50 -3
  303. package/clis/zhihu/download.js +1 -1
  304. package/clis/zhihu/question.js +42 -17
  305. package/clis/zhihu/question.test.js +113 -11
  306. package/clis/zhihu/search.js +195 -43
  307. package/clis/zhihu/search.test.js +198 -0
  308. package/clis/zhihu/text.js +29 -0
  309. package/clis/zhihu/text.test.js +24 -0
  310. package/dist/src/browser/errors.js +4 -2
  311. package/dist/src/browser/errors.test.js +6 -0
  312. package/dist/src/browser/network-cache.js +13 -1
  313. package/dist/src/browser/network-cache.test.js +17 -0
  314. package/dist/src/browser/page.js +30 -4
  315. package/dist/src/browser/page.test.js +42 -0
  316. package/dist/src/browser/utils.d.ts +1 -1
  317. package/dist/src/cli-argv-preprocess.d.ts +26 -0
  318. package/dist/src/cli-argv-preprocess.js +138 -0
  319. package/dist/src/cli-argv-preprocess.test.js +79 -0
  320. package/dist/src/convention-audit.js +15 -8
  321. package/dist/src/convention-audit.test.js +21 -0
  322. package/dist/src/download/index.js +13 -1
  323. package/dist/src/download/index.test.js +23 -1
  324. package/dist/src/download/media-download.js +15 -2
  325. package/dist/src/download/media-download.test.d.ts +1 -0
  326. package/dist/src/download/media-download.test.js +112 -0
  327. package/dist/src/download/progress.js +2 -2
  328. package/dist/src/download/progress.test.js +12 -1
  329. package/dist/src/electron-apps.js +1 -1
  330. package/dist/src/electron-apps.test.js +7 -2
  331. package/dist/src/errors.d.ts +17 -0
  332. package/dist/src/errors.js +22 -0
  333. package/dist/src/external-clis.yaml +8 -0
  334. package/dist/src/main.js +14 -2
  335. package/dist/src/output.js +11 -1
  336. package/dist/src/output.test.js +6 -0
  337. package/dist/src/registry.js +1 -0
  338. package/dist/src/registry.test.js +11 -0
  339. package/dist/src/utils.d.ts +43 -0
  340. package/dist/src/utils.js +97 -0
  341. package/dist/src/utils.test.d.ts +1 -0
  342. package/dist/src/utils.test.js +155 -0
  343. package/package.json +8 -2
  344. package/scripts/silent-column-drop-baseline.json +0 -52
  345. package/scripts/typed-error-lint-baseline.json +28 -380
  346. package/clis/slock/_utils.js +0 -12
@@ -8,10 +8,14 @@
8
8
  * Requires: logged into creator.xiaohongshu.com in Chrome.
9
9
  */
10
10
  import { cli, Strategy } from '@jackwener/opencli/registry';
11
+ import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
11
12
  const DATE_LINE_RE = /^发布于 (\d{4}年\d{2}月\d{2}日 \d{2}:\d{2})$/;
12
13
  const METRIC_LINE_RE = /^\d+$/;
13
14
  const VISIBILITY_LINE_RE = /可见$/;
14
15
  const NOTE_ANALYZE_API_PATH = '/api/galaxy/creator/datacenter/note/analyze/list';
16
+ const NOTE_ANALYZE_PAGE_SIZE = 10;
17
+ const CAPTURE_POLL_ATTEMPTS = 20;
18
+ const CAPTURE_POLL_INTERVAL_S = 0.5;
15
19
  const NOTE_DETAIL_PAGE_URL = 'https://creator.xiaohongshu.com/statistics/note-detail';
16
20
  const NOTE_ID_HTML_RE = /"noteId":"([0-9a-f]{24})"/g;
17
21
  function buildNoteDetailUrl(noteId) {
@@ -104,6 +108,237 @@ function mapAnalyzeItems(items) {
104
108
  url: buildNoteDetailUrl(item.id),
105
109
  }));
106
110
  }
111
+ function unwrapEvaluateResult(payload) {
112
+ if (payload && typeof payload === 'object' && !Array.isArray(payload) && 'session' in payload && 'data' in payload) {
113
+ return payload.data;
114
+ }
115
+ return payload;
116
+ }
117
+ // Capture the dashboard's signed /api/galaxy/* responses on window.__xhsCapture
118
+ // since a direct fetch() from page.evaluate bypasses the x-s signing and gets 406.
119
+ async function installXhsFetchCaptureHook(page) {
120
+ await page.evaluate(`(() => {
121
+ window.__xhsCapture = {};
122
+ if (window.__xhsCaptureInstalled) return;
123
+ window.__xhsCaptureInstalled = true;
124
+ const origFetch = window.fetch;
125
+ window.fetch = async function(...args) {
126
+ const resp = await origFetch.apply(this, args);
127
+ try {
128
+ const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
129
+ if (url.includes('/api/galaxy/')) {
130
+ resp.clone().text().then((body) => {
131
+ try { window.__xhsCapture[url] = { status: resp.status, ok: resp.ok, body }; } catch (_) {}
132
+ }).catch(() => {});
133
+ }
134
+ } catch (_) {}
135
+ return resp;
136
+ };
137
+ const OrigXHR = window.XMLHttpRequest;
138
+ function HookedXHR() {
139
+ const xhr = new OrigXHR();
140
+ const origOpen = xhr.open;
141
+ let capturedUrl = '';
142
+ xhr.open = function(method, url, ...rest) {
143
+ capturedUrl = url;
144
+ return origOpen.call(this, method, url, ...rest);
145
+ };
146
+ xhr.addEventListener('load', () => {
147
+ try {
148
+ if (capturedUrl.includes('/api/galaxy/')) {
149
+ window.__xhsCapture[capturedUrl] = { status: xhr.status, ok: xhr.status >= 200 && xhr.status < 300, body: xhr.responseText };
150
+ }
151
+ } catch (_) {}
152
+ });
153
+ return xhr;
154
+ }
155
+ HookedXHR.prototype = OrigXHR.prototype;
156
+ for (const key of ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE']) {
157
+ if (key in OrigXHR) HookedXHR[key] = OrigXHR[key];
158
+ }
159
+ window.XMLHttpRequest = HookedXHR;
160
+ })()`);
161
+ }
162
+ function parseCaptureMapPayload(raw) {
163
+ const payload = unwrapEvaluateResult(raw);
164
+ if (typeof payload === 'string') {
165
+ try {
166
+ return JSON.parse(payload);
167
+ }
168
+ catch {
169
+ return {};
170
+ }
171
+ }
172
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
173
+ return payload;
174
+ }
175
+ return {};
176
+ }
177
+ function getAnalyzeListPageNumber(url) {
178
+ try {
179
+ const parsed = new URL(url, 'https://creator.xiaohongshu.com');
180
+ const pageNum = Number.parseInt(parsed.searchParams.get('page_num') || '', 10);
181
+ if (Number.isFinite(pageNum) && pageNum > 0)
182
+ return pageNum;
183
+ }
184
+ catch { }
185
+ const match = String(url || '').match(/[?&]page_num=(\d+)/);
186
+ const pageNum = Number.parseInt(match?.[1] || '', 10);
187
+ return Number.isFinite(pageNum) && pageNum > 0 ? pageNum : Number.MAX_SAFE_INTEGER;
188
+ }
189
+ function harvestAnalyzeListCaptures(captureMap) {
190
+ const items = [];
191
+ const seen = new Set();
192
+ let total = 0;
193
+ const entries = Object.entries(captureMap)
194
+ .filter(([url]) => url.includes('/note/analyze/list'))
195
+ .sort(([a], [b]) => getAnalyzeListPageNumber(a) - getAnalyzeListPageNumber(b));
196
+ for (const [url, capture] of entries) {
197
+ if (!capture?.ok) continue;
198
+ try {
199
+ const json = JSON.parse(capture.body);
200
+ const data = json?.data ?? {};
201
+ if (typeof data.total === 'number' && data.total > total) total = data.total;
202
+ for (const note of data.note_infos ?? []) {
203
+ if (!note?.id || seen.has(note.id)) continue;
204
+ seen.add(note.id);
205
+ items.push(note);
206
+ }
207
+ }
208
+ catch { }
209
+ }
210
+ return { items, total };
211
+ }
212
+ function isAnalyzeCaptureComplete(items, total, limit) {
213
+ if (total <= 0)
214
+ return true;
215
+ return items.length >= Math.min(total, limit);
216
+ }
217
+ async function pollCaptureMap(page) {
218
+ let captureMap = {};
219
+ for (let i = 0; i < CAPTURE_POLL_ATTEMPTS; i++) {
220
+ await page.wait(CAPTURE_POLL_INTERVAL_S);
221
+ const raw = await page.evaluate('JSON.stringify(window.__xhsCapture || {})');
222
+ captureMap = parseCaptureMapPayload(raw);
223
+ if (Object.keys(captureMap).some((url) => url.includes('/note/analyze/list'))) break;
224
+ }
225
+ return captureMap;
226
+ }
227
+ // Fresh-published notes return title: "" from /note/analyze/list. Scrape the
228
+ // /new/note-manager card DOM (under its "全部笔记" tab, which surfaces every
229
+ // state including 审核中) so the rows the API leaves empty still get the
230
+ // derived title that the note-manager UI shows.
231
+ async function fetchNoteManagerTitleMap(page, neededCount) {
232
+ const map = new Map();
233
+ const scrapeCards = async () => {
234
+ const cards = unwrapEvaluateResult(await page.evaluate(`() => {
235
+ const noteIdRe = /"noteId":"([0-9a-f]{24})"/;
236
+ return Array.from(document.querySelectorAll('div.note[data-impression], div.note')).map((card) => {
237
+ const impression = card.getAttribute('data-impression') || '';
238
+ const id = impression.match(noteIdRe)?.[1] || '';
239
+ const title = (card.querySelector('.title, .raw')?.innerText || '').trim();
240
+ return { id, title };
241
+ }).filter((entry) => entry.id && entry.title);
242
+ }`));
243
+ for (const card of Array.isArray(cards) ? cards : []) {
244
+ if (!map.has(card.id)) map.set(card.id, card.title);
245
+ }
246
+ };
247
+ // Scroll the first scrollable ancestor of a note card to the bottom so
248
+ // the list lazy-loads the rest of its rows. Page-level scrollTo does not
249
+ // work because the cards live inside an inner overflow-auto container.
250
+ const scrollInnerListToBottom = async () => {
251
+ return unwrapEvaluateResult(await page.evaluate(`(() => {
252
+ const firstCard = document.querySelector('div.note[data-impression]');
253
+ let el = firstCard && firstCard.parentElement;
254
+ while (el) {
255
+ const s = window.getComputedStyle(el);
256
+ if ((s.overflowY === 'auto' || s.overflowY === 'scroll') && el.scrollHeight > el.clientHeight + 10) {
257
+ el.scrollTop = el.scrollHeight;
258
+ return true;
259
+ }
260
+ el = el.parentElement;
261
+ }
262
+ return false;
263
+ })()`));
264
+ };
265
+ try {
266
+ await page.goto('https://creator.xiaohongshu.com/new/note-manager');
267
+ // Poll for the initial hydration batch and then scroll the inner list
268
+ // container to surface the rest of the rows. The all-notes tab is the
269
+ // default state so no tab click is needed here.
270
+ for (let i = 0; i < 12; i++) {
271
+ await page.wait(1);
272
+ await scrapeCards();
273
+ if (map.size >= neededCount) return map;
274
+ await scrollInnerListToBottom();
275
+ }
276
+ return map;
277
+ }
278
+ catch {
279
+ return map;
280
+ }
281
+ }
282
+ async function fetchCreatorNotesByCapture(page, limit) {
283
+ // Land on dashboard root before installing the hook so the data-analysis
284
+ // SPA navigation fires page_num=1's signed request UNDER the hook.
285
+ await page.goto('https://creator.xiaohongshu.com/statistics');
286
+ await installXhsFetchCaptureHook(page);
287
+ await page.evaluate(`(() => {
288
+ history.pushState({}, '', '/statistics/data-analysis?source=official');
289
+ window.dispatchEvent(new PopStateEvent('popstate'));
290
+ })()`);
291
+ let captureMap = await pollCaptureMap(page);
292
+ let { items, total } = harvestAnalyzeListCaptures(captureMap);
293
+ if (items.length === 0) return [];
294
+ const totalPages = total > 0 ? Math.ceil(total / NOTE_ANALYZE_PAGE_SIZE) : 1;
295
+ const neededPages = Math.min(totalPages, Math.ceil(limit / NOTE_ANALYZE_PAGE_SIZE));
296
+ for (let pageNum = 2; pageNum <= neededPages && items.length < limit; pageNum++) {
297
+ const clicked = unwrapEvaluateResult(await page.evaluate(`(() => {
298
+ const target = String(${pageNum});
299
+ // .d-pagination-page renders the page number doubled (a visible span +
300
+ // an accessibility span), so textContent for page 2 reads "22". Match
301
+ // both the raw digit and the doubled form to tolerate either render.
302
+ const btns = Array.from(document.querySelectorAll('.d-pagination-page'));
303
+ const match = btns.find((btn) => {
304
+ const text = (btn.textContent || '').trim();
305
+ return text === target || text === target + target;
306
+ });
307
+ if (match) { match.click(); return true; }
308
+ return false;
309
+ })()`));
310
+ if (!clicked) break;
311
+ const before = items.length;
312
+ let advanced = false;
313
+ for (let attempt = 0; attempt < CAPTURE_POLL_ATTEMPTS; attempt++) {
314
+ await page.wait(CAPTURE_POLL_INTERVAL_S);
315
+ const raw = await page.evaluate('JSON.stringify(window.__xhsCapture || {})');
316
+ captureMap = parseCaptureMapPayload(raw);
317
+ const harvested = harvestAnalyzeListCaptures(captureMap);
318
+ if (harvested.items.length > before) {
319
+ items = harvested.items;
320
+ total = Math.max(total, harvested.total);
321
+ advanced = true;
322
+ break;
323
+ }
324
+ }
325
+ if (!advanced) break;
326
+ }
327
+ if (!isAnalyzeCaptureComplete(items, total, limit)) {
328
+ throw new CommandExecutionError(`xiaohongshu creator-notes: captured ${items.length} of ${Math.min(total, limit)} expected analyze rows; refusing partial results`);
329
+ }
330
+ const notes = mapAnalyzeItems(items).slice(0, limit);
331
+ const missingTitles = notes.filter((note) => !note.title).length;
332
+ if (missingTitles > 0) {
333
+ const titleMap = await fetchNoteManagerTitleMap(page, notes.length);
334
+ for (const note of notes) {
335
+ if (!note.title && note.id && titleMap.has(note.id)) {
336
+ note.title = titleMap.get(note.id);
337
+ }
338
+ }
339
+ }
340
+ return notes;
341
+ }
107
342
  async function fetchCreatorNotesByApi(page, limit) {
108
343
  const pageSize = Math.min(Math.max(limit, 10), 20);
109
344
  const maxPages = Math.max(1, Math.ceil(limit / pageSize));
@@ -147,7 +382,16 @@ async function fetchCreatorNotesByApi(page, limit) {
147
382
  return notes.slice(0, limit);
148
383
  }
149
384
  export async function fetchCreatorNotes(page, limit) {
150
- let notes = await fetchCreatorNotesByApi(page, limit);
385
+ let notes = [];
386
+ try {
387
+ notes = await fetchCreatorNotesByCapture(page, limit);
388
+ }
389
+ catch (error) {
390
+ if (error instanceof CommandExecutionError) throw error;
391
+ }
392
+ if (notes.length === 0) {
393
+ notes = await fetchCreatorNotesByApi(page, limit);
394
+ }
151
395
  if (notes.length === 0) {
152
396
  await page.goto('https://creator.xiaohongshu.com/new/note-manager');
153
397
  const maxPageDowns = Math.max(0, Math.ceil(limit / 10) + 1);
@@ -210,7 +454,7 @@ cli({
210
454
  const limit = kwargs.limit || 20;
211
455
  const notes = await fetchCreatorNotes(page, limit);
212
456
  if (!Array.isArray(notes) || notes.length === 0) {
213
- throw new Error('No notes found. Are you logged into creator.xiaohongshu.com?');
457
+ throw new EmptyResultError('xiaohongshu creator-notes', 'No notes found. Ensure you are logged into creator.xiaohongshu.com and the account has published notes.');
214
458
  }
215
459
  return notes
216
460
  .slice(0, limit)
@@ -227,3 +471,9 @@ cli({
227
471
  }));
228
472
  },
229
473
  });
474
+ export const __test__ = {
475
+ harvestAnalyzeListCaptures,
476
+ isAnalyzeCaptureComplete,
477
+ parseCaptureMapPayload,
478
+ unwrapEvaluateResult,
479
+ };
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
+ import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
2
3
  import { getRegistry } from '@jackwener/opencli/registry';
3
- import { parseCreatorNoteIdsFromHtml, parseCreatorNotesText } from './creator-notes.js';
4
+ import { __test__, parseCreatorNoteIdsFromHtml, parseCreatorNotesText } from './creator-notes.js';
4
5
  import './creator-notes.js';
5
6
  function createPageMock(evaluateResult, interceptedRequests = []) {
6
7
  const evaluate = Array.isArray(evaluateResult)
@@ -189,4 +190,92 @@ describe('xiaohongshu creator-notes', () => {
189
190
  'dddddddddddddddddddddddd',
190
191
  ]);
191
192
  });
193
+ it('harvests captured analyze pages in page order and dedupes note ids', () => {
194
+ const captureMap = {
195
+ '/api/galaxy/creator/datacenter/note/analyze/list?type=0&page_size=10&page_num=2': {
196
+ ok: true,
197
+ body: JSON.stringify({
198
+ data: {
199
+ total: 3,
200
+ note_infos: [
201
+ { id: 'bbbbbbbbbbbbbbbbbbbbbbbb', title: 'page 2' },
202
+ { id: 'aaaaaaaaaaaaaaaaaaaaaaaa', title: 'duplicate from page 2' },
203
+ ],
204
+ },
205
+ }),
206
+ },
207
+ '/api/galaxy/creator/datacenter/note/analyze/list?type=0&page_size=10&page_num=1': {
208
+ ok: true,
209
+ body: JSON.stringify({
210
+ data: {
211
+ total: 3,
212
+ note_infos: [
213
+ { id: 'aaaaaaaaaaaaaaaaaaaaaaaa', title: 'page 1' },
214
+ ],
215
+ },
216
+ }),
217
+ },
218
+ };
219
+ expect(__test__.harvestAnalyzeListCaptures(captureMap)).toEqual({
220
+ total: 3,
221
+ items: [
222
+ { id: 'aaaaaaaaaaaaaaaaaaaaaaaa', title: 'page 1' },
223
+ { id: 'bbbbbbbbbbbbbbbbbbbbbbbb', title: 'page 2' },
224
+ ],
225
+ });
226
+ });
227
+ it('treats incomplete captured pagination as fallback-needed instead of partial success', () => {
228
+ const firstPageItems = Array.from({ length: 10 }, (_, index) => ({
229
+ id: String(index).padStart(24, '0'),
230
+ }));
231
+ expect(__test__.isAnalyzeCaptureComplete(firstPageItems, 25, 20)).toBe(false);
232
+ expect(__test__.isAnalyzeCaptureComplete(firstPageItems, 25, 10)).toBe(true);
233
+ expect(__test__.isAnalyzeCaptureComplete(firstPageItems, 0, 20)).toBe(true);
234
+ });
235
+ it('unwraps browser bridge capture-map envelopes', () => {
236
+ const captureMap = {
237
+ '/api/galaxy/creator/datacenter/note/analyze/list?page_num=1': {
238
+ ok: true,
239
+ body: '{"data":{"total":0,"note_infos":[]}}',
240
+ },
241
+ };
242
+ expect(__test__.parseCaptureMapPayload({ session: 'site:xiaohongshu', data: JSON.stringify(captureMap) })).toEqual(captureMap);
243
+ expect(__test__.parseCaptureMapPayload({ session: 'site:xiaohongshu', data: captureMap })).toEqual(captureMap);
244
+ });
245
+ it('does not fall back to partial DOM rows when captured total proves pagination is incomplete', async () => {
246
+ const cmd = getRegistry().get('xiaohongshu/creator-notes');
247
+ const captureMap = {
248
+ '/api/galaxy/creator/datacenter/note/analyze/list?type=0&page_size=10&page_num=1': {
249
+ ok: true,
250
+ body: JSON.stringify({
251
+ data: {
252
+ total: 25,
253
+ note_infos: Array.from({ length: 10 }, (_, index) => ({
254
+ id: String(index).padStart(24, '0'),
255
+ title: `note ${index}`,
256
+ })),
257
+ },
258
+ }),
259
+ },
260
+ };
261
+ const page = createPageMock(undefined);
262
+ page.evaluate = vi.fn()
263
+ .mockResolvedValueOnce(undefined)
264
+ .mockResolvedValueOnce(undefined)
265
+ .mockResolvedValueOnce(JSON.stringify(captureMap))
266
+ .mockResolvedValueOnce(false);
267
+
268
+ await expect(cmd.func(page, { limit: 20 })).rejects.toBeInstanceOf(CommandExecutionError);
269
+ });
270
+ it('throws EmptyResultError when the creator account has no notes', async () => {
271
+ const cmd = getRegistry().get('xiaohongshu/creator-notes');
272
+ const page = createPageMock(undefined);
273
+ page.evaluate = vi.fn()
274
+ .mockResolvedValueOnce(undefined)
275
+ .mockResolvedValueOnce(undefined)
276
+ .mockResolvedValueOnce([])
277
+ .mockResolvedValueOnce({ text: '', html: '' });
278
+
279
+ await expect(cmd.func(page, { limit: 1 })).rejects.toBeInstanceOf(EmptyResultError);
280
+ });
192
281
  });
@@ -8,6 +8,7 @@
8
8
  * Requires: logged into creator.xiaohongshu.com in Chrome.
9
9
  */
10
10
  import { cli, Strategy } from '@jackwener/opencli/registry';
11
+ import { EmptyResultError } from '@jackwener/opencli/errors';
11
12
  cli({
12
13
  site: 'xiaohongshu',
13
14
  name: 'creator-stats',
@@ -52,7 +53,7 @@ cli({
52
53
  }
53
54
  const stats = data.data[period];
54
55
  if (!stats) {
55
- throw new Error(`No data for period "${period}". Available: ${Object.keys(data.data).join(', ')}`);
56
+ throw new EmptyResultError('xiaohongshu creator-stats', `No data for period "${period}". Available: ${Object.keys(data.data).join(', ')}`);
56
57
  }
57
58
  // Format daily trend as sparkline-like summary
58
59
  const formatTrend = (list) => {
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { EmptyResultError } from '@jackwener/opencli/errors';
3
+ import { getRegistry } from '@jackwener/opencli/registry';
4
+ import './creator-stats.js';
5
+
6
+ describe('xiaohongshu creator-stats', () => {
7
+ it('throws EmptyResultError when the requested stats period has no data', async () => {
8
+ const cmd = getRegistry().get('xiaohongshu/creator-stats');
9
+ const page = {
10
+ goto: vi.fn().mockResolvedValue(undefined),
11
+ evaluate: vi.fn().mockResolvedValue({
12
+ data: {
13
+ seven: null,
14
+ thirty: {
15
+ view_count: 1,
16
+ view_list: [{ count: 1 }],
17
+ },
18
+ },
19
+ }),
20
+ };
21
+
22
+ await expect(cmd.func(page, { period: 'seven' })).rejects.toBeInstanceOf(EmptyResultError);
23
+ });
24
+ });