@jackwener/opencli 1.5.5 → 1.5.7

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 (540) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +31 -4
  3. package/README.zh-CN.md +40 -5
  4. package/SKILL.md +1 -1
  5. package/dist/browser/cdp.d.ts +1 -0
  6. package/dist/browser/cdp.js +30 -27
  7. package/dist/browser/daemon-client.d.ts +11 -1
  8. package/dist/browser/daemon-client.js +3 -0
  9. package/dist/browser/dom-helpers.js +1 -0
  10. package/dist/browser/dom-helpers.test.js +14 -1
  11. package/dist/browser/mcp.js +18 -13
  12. package/dist/browser/page.d.ts +6 -0
  13. package/dist/browser/page.js +37 -2
  14. package/dist/browser/page.test.d.ts +1 -0
  15. package/dist/browser/page.test.js +44 -0
  16. package/dist/browser/stealth.js +198 -0
  17. package/dist/browser/stealth.test.d.ts +1 -0
  18. package/dist/browser/stealth.test.js +134 -0
  19. package/dist/browser.test.js +1 -1
  20. package/dist/build-manifest.d.ts +1 -0
  21. package/dist/build-manifest.js +5 -1
  22. package/dist/build-manifest.test.js +2 -0
  23. package/dist/cli-manifest.json +1821 -252
  24. package/dist/cli.js +20 -3
  25. package/dist/clis/antigravity/serve.d.ts +1 -1
  26. package/dist/clis/antigravity/serve.js +5 -8
  27. package/dist/clis/band/bands.d.ts +1 -0
  28. package/dist/clis/band/bands.js +72 -0
  29. package/dist/clis/band/mentions.d.ts +1 -0
  30. package/dist/clis/band/mentions.js +127 -0
  31. package/dist/clis/band/post.d.ts +1 -0
  32. package/dist/clis/band/post.js +175 -0
  33. package/dist/clis/band/posts.d.ts +1 -0
  34. package/dist/clis/band/posts.js +94 -0
  35. package/dist/clis/bilibili/subtitle.js +4 -0
  36. package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
  37. package/dist/clis/bilibili/subtitle.test.js +48 -0
  38. package/dist/clis/chatwise/ask.js +0 -2
  39. package/dist/clis/chatwise/export.js +0 -2
  40. package/dist/clis/chatwise/history.js +0 -2
  41. package/dist/clis/chatwise/model.js +0 -2
  42. package/dist/clis/chatwise/new.js +1 -2
  43. package/dist/clis/chatwise/read.js +0 -2
  44. package/dist/clis/chatwise/screenshot.js +1 -2
  45. package/dist/clis/chatwise/send.js +0 -2
  46. package/dist/clis/chatwise/status.js +1 -2
  47. package/dist/clis/ctrip/search.d.ts +13 -0
  48. package/dist/clis/ctrip/search.js +73 -48
  49. package/dist/clis/ctrip/search.test.d.ts +1 -0
  50. package/dist/clis/ctrip/search.test.js +64 -0
  51. package/dist/clis/doubao/detail.d.ts +1 -0
  52. package/dist/clis/doubao/detail.js +33 -0
  53. package/dist/clis/doubao/detail.test.d.ts +1 -0
  54. package/dist/clis/doubao/detail.test.js +42 -0
  55. package/dist/clis/doubao/history.d.ts +1 -0
  56. package/dist/clis/doubao/history.js +28 -0
  57. package/dist/clis/doubao/history.test.d.ts +1 -0
  58. package/dist/clis/doubao/history.test.js +37 -0
  59. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  60. package/dist/clis/doubao/meeting-summary.js +39 -0
  61. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  62. package/dist/clis/doubao/meeting-transcript.js +36 -0
  63. package/dist/clis/doubao/utils.d.ts +27 -0
  64. package/dist/clis/doubao/utils.js +317 -0
  65. package/dist/clis/doubao/utils.test.d.ts +1 -0
  66. package/dist/clis/doubao/utils.test.js +24 -0
  67. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  68. package/dist/clis/douyin/_shared/public-api.js +29 -0
  69. package/dist/clis/douyin/_shared/sts2.js +8 -2
  70. package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
  71. package/dist/clis/douyin/_shared/sts2.test.js +27 -0
  72. package/dist/clis/douyin/activities.js +4 -2
  73. package/dist/clis/douyin/activities.test.js +34 -1
  74. package/dist/clis/douyin/collections.js +1 -1
  75. package/dist/clis/douyin/collections.test.js +24 -2
  76. package/dist/clis/douyin/draft.d.ts +8 -11
  77. package/dist/clis/douyin/draft.js +302 -185
  78. package/dist/clis/douyin/draft.test.d.ts +1 -1
  79. package/dist/clis/douyin/draft.test.js +357 -2
  80. package/dist/clis/douyin/hashtag.js +9 -2
  81. package/dist/clis/douyin/hashtag.test.js +35 -2
  82. package/dist/clis/douyin/profile.js +1 -1
  83. package/dist/clis/douyin/profile.test.js +36 -1
  84. package/dist/clis/douyin/user-videos.d.ts +5 -0
  85. package/dist/clis/douyin/user-videos.js +74 -0
  86. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  87. package/dist/clis/douyin/user-videos.test.js +108 -0
  88. package/dist/clis/douyin/videos.js +22 -5
  89. package/dist/clis/douyin/videos.test.js +45 -2
  90. package/dist/clis/facebook/search.test.d.ts +5 -0
  91. package/dist/clis/facebook/search.test.js +60 -0
  92. package/dist/clis/facebook/search.yaml +4 -3
  93. package/dist/clis/instagram/download.d.ts +16 -0
  94. package/dist/clis/instagram/download.js +225 -0
  95. package/dist/clis/instagram/download.test.d.ts +1 -0
  96. package/dist/clis/instagram/download.test.js +118 -0
  97. package/dist/clis/notebooklm/bind-current.d.ts +1 -0
  98. package/dist/clis/notebooklm/bind-current.js +29 -0
  99. package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
  100. package/dist/clis/notebooklm/bind-current.test.js +35 -0
  101. package/dist/clis/notebooklm/binding.test.d.ts +1 -0
  102. package/dist/clis/notebooklm/binding.test.js +44 -0
  103. package/dist/clis/notebooklm/compat.test.d.ts +3 -0
  104. package/dist/clis/notebooklm/compat.test.js +16 -0
  105. package/dist/clis/notebooklm/current.d.ts +1 -0
  106. package/dist/clis/notebooklm/current.js +28 -0
  107. package/dist/clis/notebooklm/get.d.ts +1 -0
  108. package/dist/clis/notebooklm/get.js +37 -0
  109. package/dist/clis/notebooklm/history.d.ts +1 -0
  110. package/dist/clis/notebooklm/history.js +25 -0
  111. package/dist/clis/notebooklm/history.test.d.ts +1 -0
  112. package/dist/clis/notebooklm/history.test.js +58 -0
  113. package/dist/clis/notebooklm/list.d.ts +1 -0
  114. package/dist/clis/notebooklm/list.js +35 -0
  115. package/dist/clis/notebooklm/note-list.d.ts +1 -0
  116. package/dist/clis/notebooklm/note-list.js +28 -0
  117. package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
  118. package/dist/clis/notebooklm/note-list.test.js +56 -0
  119. package/dist/clis/notebooklm/notes-get.d.ts +1 -0
  120. package/dist/clis/notebooklm/notes-get.js +47 -0
  121. package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
  122. package/dist/clis/notebooklm/notes-get.test.js +72 -0
  123. package/dist/clis/notebooklm/rpc.d.ts +36 -0
  124. package/dist/clis/notebooklm/rpc.js +189 -0
  125. package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
  126. package/dist/clis/notebooklm/rpc.test.js +105 -0
  127. package/dist/clis/notebooklm/shared.d.ts +87 -0
  128. package/dist/clis/notebooklm/shared.js +3 -0
  129. package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
  130. package/dist/clis/notebooklm/source-fulltext.js +44 -0
  131. package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
  132. package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
  133. package/dist/clis/notebooklm/source-get.d.ts +1 -0
  134. package/dist/clis/notebooklm/source-get.js +40 -0
  135. package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
  136. package/dist/clis/notebooklm/source-get.test.js +84 -0
  137. package/dist/clis/notebooklm/source-guide.d.ts +1 -0
  138. package/dist/clis/notebooklm/source-guide.js +44 -0
  139. package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
  140. package/dist/clis/notebooklm/source-guide.test.js +104 -0
  141. package/dist/clis/notebooklm/source-list.d.ts +1 -0
  142. package/dist/clis/notebooklm/source-list.js +30 -0
  143. package/dist/clis/notebooklm/status.d.ts +1 -0
  144. package/dist/clis/notebooklm/status.js +31 -0
  145. package/dist/clis/notebooklm/summary.d.ts +1 -0
  146. package/dist/clis/notebooklm/summary.js +30 -0
  147. package/dist/clis/notebooklm/summary.test.d.ts +1 -0
  148. package/dist/clis/notebooklm/summary.test.js +78 -0
  149. package/dist/clis/notebooklm/utils.d.ts +37 -0
  150. package/dist/clis/notebooklm/utils.js +739 -0
  151. package/dist/clis/notebooklm/utils.test.d.ts +1 -0
  152. package/dist/clis/notebooklm/utils.test.js +390 -0
  153. package/dist/clis/ones/common.d.ts +32 -0
  154. package/dist/clis/ones/common.js +144 -0
  155. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  156. package/dist/clis/ones/enrich-tasks.js +37 -0
  157. package/dist/clis/ones/login.d.ts +1 -0
  158. package/dist/clis/ones/login.js +80 -0
  159. package/dist/clis/ones/logout.d.ts +1 -0
  160. package/dist/clis/ones/logout.js +17 -0
  161. package/dist/clis/ones/me.d.ts +1 -0
  162. package/dist/clis/ones/me.js +30 -0
  163. package/dist/clis/ones/my-tasks.d.ts +1 -0
  164. package/dist/clis/ones/my-tasks.js +120 -0
  165. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  166. package/dist/clis/ones/resolve-labels.js +64 -0
  167. package/dist/clis/ones/task-helpers.d.ts +29 -0
  168. package/dist/clis/ones/task-helpers.js +212 -0
  169. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  170. package/dist/clis/ones/task-helpers.test.js +12 -0
  171. package/dist/clis/ones/task.d.ts +1 -0
  172. package/dist/clis/ones/task.js +66 -0
  173. package/dist/clis/ones/tasks.d.ts +1 -0
  174. package/dist/clis/ones/tasks.js +79 -0
  175. package/dist/clis/ones/token-info.d.ts +1 -0
  176. package/dist/clis/ones/token-info.js +42 -0
  177. package/dist/clis/ones/worklog.d.ts +11 -0
  178. package/dist/clis/ones/worklog.js +267 -0
  179. package/dist/clis/ones/worklog.test.d.ts +1 -0
  180. package/dist/clis/ones/worklog.test.js +20 -0
  181. package/dist/clis/spotify/spotify.d.ts +1 -0
  182. package/dist/clis/spotify/spotify.js +316 -0
  183. package/dist/clis/spotify/utils.d.ts +21 -0
  184. package/dist/clis/spotify/utils.js +66 -0
  185. package/dist/clis/spotify/utils.test.d.ts +1 -0
  186. package/dist/clis/spotify/utils.test.js +67 -0
  187. package/dist/clis/substack/utils.d.ts +4 -0
  188. package/dist/clis/substack/utils.js +8 -2
  189. package/dist/clis/substack/utils.test.d.ts +1 -0
  190. package/dist/clis/substack/utils.test.js +46 -0
  191. package/dist/clis/tieba/commands.test.d.ts +4 -0
  192. package/dist/clis/tieba/commands.test.js +79 -0
  193. package/dist/clis/tieba/hot.d.ts +1 -0
  194. package/dist/clis/tieba/hot.js +48 -0
  195. package/dist/clis/tieba/posts.d.ts +1 -0
  196. package/dist/clis/tieba/posts.js +85 -0
  197. package/dist/clis/tieba/read.d.ts +1 -0
  198. package/dist/clis/tieba/read.js +140 -0
  199. package/dist/clis/tieba/search.d.ts +1 -0
  200. package/dist/clis/tieba/search.js +108 -0
  201. package/dist/clis/tieba/utils.d.ts +101 -0
  202. package/dist/clis/tieba/utils.js +240 -0
  203. package/dist/clis/tieba/utils.test.d.ts +1 -0
  204. package/dist/clis/tieba/utils.test.js +290 -0
  205. package/dist/clis/v2ex/hot.yaml +4 -1
  206. package/dist/clis/v2ex/latest.yaml +4 -1
  207. package/dist/clis/v2ex/topic.yaml +6 -1
  208. package/dist/clis/weixin/download.d.ts +9 -0
  209. package/dist/clis/weixin/download.js +76 -6
  210. package/dist/clis/weread/book.js +206 -13
  211. package/dist/clis/weread/commands.test.js +331 -0
  212. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  213. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  214. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  215. package/dist/clis/weread/search-regression.test.js +407 -0
  216. package/dist/clis/weread/search.js +143 -7
  217. package/dist/clis/weread/shelf.js +13 -95
  218. package/dist/clis/weread/utils.d.ts +56 -0
  219. package/dist/clis/weread/utils.js +234 -7
  220. package/dist/clis/weread/utils.test.js +71 -1
  221. package/dist/clis/xiaohongshu/comments.d.ts +3 -0
  222. package/dist/clis/xiaohongshu/comments.js +76 -17
  223. package/dist/clis/xiaohongshu/comments.test.js +70 -9
  224. package/dist/clis/xiaohongshu/download.d.ts +4 -1
  225. package/dist/clis/xiaohongshu/download.js +83 -22
  226. package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
  227. package/dist/clis/xiaohongshu/download.test.js +75 -0
  228. package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
  229. package/dist/clis/xiaohongshu/note-helpers.js +23 -0
  230. package/dist/clis/xiaohongshu/note.d.ts +7 -0
  231. package/dist/clis/xiaohongshu/note.js +76 -0
  232. package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
  233. package/dist/clis/xiaohongshu/note.test.js +136 -0
  234. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  235. package/dist/clis/xiaohongshu/publish.js +78 -31
  236. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  237. package/dist/clis/xiaohongshu/search.js +9 -0
  238. package/dist/clis/xiaohongshu/search.test.js +10 -4
  239. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  240. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  241. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  242. package/dist/clis/xueqiu/comments.d.ts +118 -0
  243. package/dist/clis/xueqiu/comments.js +354 -0
  244. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  245. package/dist/clis/xueqiu/comments.test.js +696 -0
  246. package/dist/clis/youtube/search.js +57 -17
  247. package/dist/clis/youtube/transcript.js +2 -4
  248. package/dist/clis/youtube/utils.d.ts +9 -0
  249. package/dist/clis/youtube/utils.js +67 -3
  250. package/dist/clis/youtube/utils.test.d.ts +1 -0
  251. package/dist/clis/youtube/utils.test.js +37 -0
  252. package/dist/clis/youtube/video.js +16 -15
  253. package/dist/clis/zhihu/question.js +19 -17
  254. package/dist/clis/zhihu/question.test.d.ts +1 -0
  255. package/dist/clis/zhihu/question.test.js +54 -0
  256. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  257. package/dist/clis/zsxq/dynamics.js +47 -0
  258. package/dist/clis/zsxq/groups.d.ts +1 -0
  259. package/dist/clis/zsxq/groups.js +32 -0
  260. package/dist/clis/zsxq/search.d.ts +1 -0
  261. package/dist/clis/zsxq/search.js +43 -0
  262. package/dist/clis/zsxq/search.test.d.ts +1 -0
  263. package/dist/clis/zsxq/search.test.js +24 -0
  264. package/dist/clis/zsxq/topic.d.ts +1 -0
  265. package/dist/clis/zsxq/topic.js +47 -0
  266. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  267. package/dist/clis/zsxq/topic.test.js +29 -0
  268. package/dist/clis/zsxq/topics.d.ts +1 -0
  269. package/dist/clis/zsxq/topics.js +25 -0
  270. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  271. package/dist/clis/zsxq/topics.test.js +24 -0
  272. package/dist/clis/zsxq/utils.d.ts +97 -0
  273. package/dist/clis/zsxq/utils.js +230 -0
  274. package/dist/commanderAdapter.js +10 -1
  275. package/dist/commanderAdapter.test.js +64 -0
  276. package/dist/commands/daemon.d.ts +9 -0
  277. package/dist/commands/daemon.js +124 -0
  278. package/dist/commands/daemon.test.d.ts +1 -0
  279. package/dist/commands/daemon.test.js +185 -0
  280. package/dist/completion.js +3 -1
  281. package/dist/constants.d.ts +2 -0
  282. package/dist/constants.js +2 -0
  283. package/dist/daemon.d.ts +1 -1
  284. package/dist/daemon.js +25 -14
  285. package/dist/daemon.test.d.ts +1 -0
  286. package/dist/daemon.test.js +65 -0
  287. package/dist/discovery.d.ts +9 -0
  288. package/dist/discovery.js +47 -2
  289. package/dist/electron-apps.d.ts +29 -0
  290. package/dist/electron-apps.js +65 -0
  291. package/dist/electron-apps.test.d.ts +1 -0
  292. package/dist/electron-apps.test.js +43 -0
  293. package/dist/engine.test.js +41 -9
  294. package/dist/execution.js +20 -16
  295. package/dist/external-clis.yaml +17 -0
  296. package/dist/idle-manager.d.ts +19 -0
  297. package/dist/idle-manager.js +54 -0
  298. package/dist/launcher.d.ts +36 -0
  299. package/dist/launcher.js +152 -0
  300. package/dist/launcher.test.d.ts +1 -0
  301. package/dist/launcher.test.js +57 -0
  302. package/dist/main.js +3 -3
  303. package/dist/registry.d.ts +1 -0
  304. package/dist/registry.js +31 -3
  305. package/dist/registry.test.js +13 -0
  306. package/dist/runtime.d.ts +5 -3
  307. package/dist/runtime.js +12 -5
  308. package/dist/serialization.d.ts +1 -0
  309. package/dist/serialization.js +3 -0
  310. package/dist/serialization.test.js +17 -1
  311. package/dist/tui.d.ts +7 -0
  312. package/dist/tui.js +52 -0
  313. package/dist/tui.test.d.ts +1 -0
  314. package/dist/tui.test.js +19 -0
  315. package/dist/types.d.ts +5 -0
  316. package/dist/weixin-download.test.js +14 -0
  317. package/docs/.vitepress/config.mts +4 -0
  318. package/docs/adapters/browser/band.md +63 -0
  319. package/docs/adapters/browser/notebooklm.md +69 -0
  320. package/docs/adapters/browser/ones.md +59 -0
  321. package/docs/adapters/browser/spotify.md +62 -0
  322. package/docs/adapters/browser/tieba.md +45 -0
  323. package/docs/adapters/browser/xiaohongshu.md +19 -10
  324. package/docs/adapters/browser/xueqiu.md +5 -0
  325. package/docs/adapters/browser/zsxq.md +49 -0
  326. package/docs/adapters/index.md +67 -63
  327. package/docs/adapters-doc/ones.md +32 -0
  328. package/docs/guide/browser-bridge.md +12 -0
  329. package/docs/guide/troubleshooting.md +9 -4
  330. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  331. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  332. package/docs/zh/guide/browser-bridge.md +12 -0
  333. package/extension/dist/background.js +794 -513
  334. package/extension/src/background.test.ts +202 -2
  335. package/extension/src/background.ts +189 -10
  336. package/extension/src/cdp.ts +54 -0
  337. package/extension/src/protocol.ts +11 -5
  338. package/package.json +1 -1
  339. package/scripts/postinstall.js +16 -0
  340. package/src/browser/cdp.ts +24 -17
  341. package/src/browser/daemon-client.ts +11 -1
  342. package/src/browser/dom-helpers.test.ts +15 -1
  343. package/src/browser/dom-helpers.ts +1 -0
  344. package/src/browser/mcp.ts +18 -13
  345. package/src/browser/page.test.ts +58 -0
  346. package/src/browser/page.ts +34 -2
  347. package/src/browser/stealth.test.ts +153 -0
  348. package/src/browser/stealth.ts +198 -0
  349. package/src/browser.test.ts +1 -1
  350. package/src/build-manifest.test.ts +2 -0
  351. package/src/build-manifest.ts +6 -1
  352. package/src/cli.ts +21 -3
  353. package/src/clis/antigravity/SKILL.md +3 -12
  354. package/src/clis/antigravity/serve.ts +5 -10
  355. package/src/clis/band/bands.ts +76 -0
  356. package/src/clis/band/mentions.ts +134 -0
  357. package/src/clis/band/post.ts +187 -0
  358. package/src/clis/band/posts.ts +106 -0
  359. package/src/clis/bilibili/subtitle.test.ts +60 -0
  360. package/src/clis/bilibili/subtitle.ts +4 -0
  361. package/src/clis/chatwise/ask.ts +0 -2
  362. package/src/clis/chatwise/export.ts +0 -2
  363. package/src/clis/chatwise/history.ts +0 -2
  364. package/src/clis/chatwise/model.ts +0 -2
  365. package/src/clis/chatwise/new.ts +1 -2
  366. package/src/clis/chatwise/read.ts +0 -2
  367. package/src/clis/chatwise/screenshot.ts +1 -2
  368. package/src/clis/chatwise/send.ts +0 -2
  369. package/src/clis/chatwise/status.ts +1 -2
  370. package/src/clis/ctrip/search.test.ts +73 -0
  371. package/src/clis/ctrip/search.ts +97 -47
  372. package/src/clis/doubao/detail.test.ts +53 -0
  373. package/src/clis/doubao/detail.ts +41 -0
  374. package/src/clis/doubao/history.test.ts +45 -0
  375. package/src/clis/doubao/history.ts +32 -0
  376. package/src/clis/doubao/meeting-summary.ts +53 -0
  377. package/src/clis/doubao/meeting-transcript.ts +48 -0
  378. package/src/clis/doubao/utils.test.ts +45 -0
  379. package/src/clis/doubao/utils.ts +371 -0
  380. package/src/clis/douyin/_shared/public-api.ts +84 -0
  381. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  382. package/src/clis/douyin/_shared/sts2.ts +11 -3
  383. package/src/clis/douyin/activities.test.ts +41 -1
  384. package/src/clis/douyin/activities.ts +12 -3
  385. package/src/clis/douyin/collections.test.ts +35 -2
  386. package/src/clis/douyin/collections.ts +1 -1
  387. package/src/clis/douyin/draft.test.ts +444 -2
  388. package/src/clis/douyin/draft.ts +382 -218
  389. package/src/clis/douyin/hashtag.test.ts +42 -2
  390. package/src/clis/douyin/hashtag.ts +11 -3
  391. package/src/clis/douyin/profile.test.ts +43 -1
  392. package/src/clis/douyin/profile.ts +9 -2
  393. package/src/clis/douyin/user-videos.test.ts +122 -0
  394. package/src/clis/douyin/user-videos.ts +101 -0
  395. package/src/clis/douyin/videos.test.ts +52 -2
  396. package/src/clis/douyin/videos.ts +49 -15
  397. package/src/clis/facebook/search.test.ts +70 -0
  398. package/src/clis/facebook/search.yaml +4 -3
  399. package/src/clis/instagram/download.test.ts +159 -0
  400. package/src/clis/instagram/download.ts +286 -0
  401. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  402. package/src/clis/notebooklm/bind-current.ts +36 -0
  403. package/src/clis/notebooklm/binding.test.ts +53 -0
  404. package/src/clis/notebooklm/compat.test.ts +19 -0
  405. package/src/clis/notebooklm/current.ts +38 -0
  406. package/src/clis/notebooklm/get.ts +53 -0
  407. package/src/clis/notebooklm/history.test.ts +70 -0
  408. package/src/clis/notebooklm/history.ts +36 -0
  409. package/src/clis/notebooklm/list.ts +40 -0
  410. package/src/clis/notebooklm/note-list.test.ts +64 -0
  411. package/src/clis/notebooklm/note-list.ts +42 -0
  412. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  413. package/src/clis/notebooklm/notes-get.ts +67 -0
  414. package/src/clis/notebooklm/rpc.test.ts +126 -0
  415. package/src/clis/notebooklm/rpc.ts +286 -0
  416. package/src/clis/notebooklm/shared.ts +98 -0
  417. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  418. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  419. package/src/clis/notebooklm/source-get.test.ts +100 -0
  420. package/src/clis/notebooklm/source-get.ts +60 -0
  421. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  422. package/src/clis/notebooklm/source-guide.ts +69 -0
  423. package/src/clis/notebooklm/source-list.ts +45 -0
  424. package/src/clis/notebooklm/status.ts +34 -0
  425. package/src/clis/notebooklm/summary.test.ts +94 -0
  426. package/src/clis/notebooklm/summary.ts +45 -0
  427. package/src/clis/notebooklm/utils.test.ts +446 -0
  428. package/src/clis/notebooklm/utils.ts +893 -0
  429. package/src/clis/ones/common.ts +187 -0
  430. package/src/clis/ones/enrich-tasks.ts +47 -0
  431. package/src/clis/ones/login.ts +103 -0
  432. package/src/clis/ones/logout.ts +19 -0
  433. package/src/clis/ones/me.ts +34 -0
  434. package/src/clis/ones/my-tasks.ts +148 -0
  435. package/src/clis/ones/resolve-labels.ts +80 -0
  436. package/src/clis/ones/task-helpers.test.ts +14 -0
  437. package/src/clis/ones/task-helpers.ts +214 -0
  438. package/src/clis/ones/task.ts +79 -0
  439. package/src/clis/ones/tasks.ts +92 -0
  440. package/src/clis/ones/token-info.ts +46 -0
  441. package/src/clis/ones/worklog.test.ts +24 -0
  442. package/src/clis/ones/worklog.ts +306 -0
  443. package/src/clis/spotify/spotify.ts +328 -0
  444. package/src/clis/spotify/utils.test.ts +87 -0
  445. package/src/clis/spotify/utils.ts +92 -0
  446. package/src/clis/substack/utils.test.ts +54 -0
  447. package/src/clis/substack/utils.ts +10 -2
  448. package/src/clis/tieba/commands.test.ts +86 -0
  449. package/src/clis/tieba/hot.ts +52 -0
  450. package/src/clis/tieba/posts.ts +108 -0
  451. package/src/clis/tieba/read.ts +158 -0
  452. package/src/clis/tieba/search.ts +119 -0
  453. package/src/clis/tieba/utils.test.ts +322 -0
  454. package/src/clis/tieba/utils.ts +348 -0
  455. package/src/clis/v2ex/hot.yaml +4 -1
  456. package/src/clis/v2ex/latest.yaml +4 -1
  457. package/src/clis/v2ex/topic.yaml +6 -1
  458. package/src/clis/weixin/download.ts +95 -6
  459. package/src/clis/weread/book.ts +256 -13
  460. package/src/clis/weread/commands.test.ts +409 -0
  461. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  462. package/src/clis/weread/search-regression.test.ts +440 -0
  463. package/src/clis/weread/search.ts +189 -9
  464. package/src/clis/weread/shelf.ts +20 -122
  465. package/src/clis/weread/utils.test.ts +81 -1
  466. package/src/clis/weread/utils.ts +293 -7
  467. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  468. package/src/clis/xiaohongshu/comments.ts +76 -17
  469. package/src/clis/xiaohongshu/download.test.ts +96 -0
  470. package/src/clis/xiaohongshu/download.ts +83 -22
  471. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  472. package/src/clis/xiaohongshu/note.test.ts +164 -0
  473. package/src/clis/xiaohongshu/note.ts +86 -0
  474. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  475. package/src/clis/xiaohongshu/publish.ts +84 -30
  476. package/src/clis/xiaohongshu/search.test.ts +11 -4
  477. package/src/clis/xiaohongshu/search.ts +13 -0
  478. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  479. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  480. package/src/clis/xueqiu/comments.test.ts +823 -0
  481. package/src/clis/xueqiu/comments.ts +461 -0
  482. package/src/clis/youtube/search.ts +57 -17
  483. package/src/clis/youtube/transcript.ts +2 -4
  484. package/src/clis/youtube/utils.test.ts +43 -0
  485. package/src/clis/youtube/utils.ts +69 -0
  486. package/src/clis/youtube/video.ts +16 -15
  487. package/src/clis/zhihu/question.test.ts +71 -0
  488. package/src/clis/zhihu/question.ts +27 -15
  489. package/src/clis/zsxq/dynamics.ts +60 -0
  490. package/src/clis/zsxq/groups.ts +41 -0
  491. package/src/clis/zsxq/search.test.ts +29 -0
  492. package/src/clis/zsxq/search.ts +54 -0
  493. package/src/clis/zsxq/topic.test.ts +34 -0
  494. package/src/clis/zsxq/topic.ts +68 -0
  495. package/src/clis/zsxq/topics.test.ts +29 -0
  496. package/src/clis/zsxq/topics.ts +36 -0
  497. package/src/clis/zsxq/utils.ts +351 -0
  498. package/src/commanderAdapter.test.ts +77 -0
  499. package/src/commanderAdapter.ts +8 -1
  500. package/src/commands/daemon.test.ts +238 -0
  501. package/src/commands/daemon.ts +135 -0
  502. package/src/completion.ts +2 -1
  503. package/src/constants.ts +3 -0
  504. package/src/daemon.test.ts +88 -0
  505. package/src/daemon.ts +26 -14
  506. package/src/discovery.ts +52 -2
  507. package/src/electron-apps.test.ts +50 -0
  508. package/src/electron-apps.ts +89 -0
  509. package/src/engine.test.ts +45 -9
  510. package/src/execution.ts +24 -19
  511. package/src/external-clis.yaml +17 -0
  512. package/src/idle-manager.ts +60 -0
  513. package/src/launcher.test.ts +67 -0
  514. package/src/launcher.ts +185 -0
  515. package/src/main.ts +3 -2
  516. package/src/registry.test.ts +15 -0
  517. package/src/registry.ts +32 -3
  518. package/src/runtime.ts +13 -7
  519. package/src/serialization.test.ts +19 -1
  520. package/src/serialization.ts +2 -0
  521. package/src/tui.test.ts +23 -0
  522. package/src/tui.ts +65 -0
  523. package/src/types.ts +5 -0
  524. package/src/weixin-download.test.ts +27 -0
  525. package/tests/e2e/band-auth.test.ts +20 -0
  526. package/tests/e2e/browser-auth-helpers.ts +18 -0
  527. package/tests/e2e/browser-auth.test.ts +35 -47
  528. package/tests/e2e/browser-public-extended.test.ts +6 -2
  529. package/tests/e2e/browser-public.test.ts +288 -0
  530. package/tests/e2e/management.test.ts +1 -1
  531. package/tests/e2e/plugin-management.test.ts +1 -1
  532. package/vitest.config.ts +1 -0
  533. package/chatwise-opencli.ps1 +0 -82
  534. package/dist/clis/chatwise/shared.d.ts +0 -2
  535. package/dist/clis/chatwise/shared.js +0 -6
  536. package/dist/weread-private-api-regression.test.d.ts +0 -1
  537. package/dist/weread-search-regression.test.d.ts +0 -1
  538. package/dist/weread-search-regression.test.js +0 -39
  539. package/src/clis/chatwise/shared.ts +0 -8
  540. package/src/weread-search-regression.test.ts +0 -44
@@ -1,32 +1,42 @@
1
1
  /**
2
2
  * Xiaohongshu comments — DOM extraction from note detail page.
3
3
  * XHS API requires signed requests, so we scrape the rendered DOM instead.
4
+ *
5
+ * Supports both top-level comments and nested replies (楼中楼) via
6
+ * the --with-replies flag.
4
7
  */
5
8
  import { cli, Strategy } from '../../registry.js';
6
9
  import { AuthRequiredError, EmptyResultError } from '../../errors.js';
10
+ import { parseNoteId, buildNoteUrl } from './note-helpers.js';
11
+ function parseCommentLimit(raw, fallback = 20) {
12
+ const n = Number(raw);
13
+ if (!Number.isFinite(n))
14
+ return fallback;
15
+ return Math.max(1, Math.min(Math.floor(n), 50));
16
+ }
7
17
  cli({
8
18
  site: 'xiaohongshu',
9
19
  name: 'comments',
10
- description: '获取小红书笔记评论(仅主评论,不含楼中楼)',
20
+ description: '获取小红书笔记评论(支持楼中楼子回复)',
11
21
  domain: 'www.xiaohongshu.com',
12
22
  strategy: Strategy.COOKIE,
13
23
  args: [
14
- { name: 'note-id', required: true, positional: true, help: 'Note ID or full /explore/<id> URL' },
15
- { name: 'limit', type: 'int', default: 20, help: 'Number of comments (max 50)' },
24
+ { name: 'note-id', required: true, positional: true, help: 'Note ID or full URL (preserves xsec_token for access)' },
25
+ { name: 'limit', type: 'int', default: 20, help: 'Number of top-level comments (max 50)' },
26
+ { name: 'with-replies', type: 'boolean', default: false, help: 'Include nested replies (楼中楼)' },
16
27
  ],
17
- columns: ['rank', 'author', 'text', 'likes', 'time'],
28
+ columns: ['rank', 'author', 'text', 'likes', 'time', 'is_reply', 'reply_to'],
18
29
  func: async (page, kwargs) => {
19
- const limit = Math.min(Number(kwargs.limit) || 20, 50);
20
- let noteId = String(kwargs['note-id']).trim();
21
- // Accept full URLs: /explore/<id> or /note/<id>
22
- const urlMatch = noteId.match(/\/explore\/([a-f0-9]+)/) || noteId.match(/\/note\/([a-f0-9]+)/);
23
- if (urlMatch)
24
- noteId = urlMatch[1];
25
- await page.goto(`https://www.xiaohongshu.com/explore/${noteId}`);
30
+ const limit = parseCommentLimit(kwargs.limit);
31
+ const withReplies = Boolean(kwargs['with-replies']);
32
+ const raw = String(kwargs['note-id']);
33
+ const noteId = parseNoteId(raw);
34
+ await page.goto(buildNoteUrl(raw));
26
35
  await page.wait(3);
27
36
  const data = await page.evaluate(`
28
37
  (async () => {
29
38
  const wait = (ms) => new Promise(r => setTimeout(r, ms))
39
+ const withReplies = ${withReplies}
30
40
 
31
41
  // Check login state
32
42
  const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
@@ -41,6 +51,31 @@ cli({
41
51
  }
42
52
 
43
53
  const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
54
+ const parseLikes = (el) => {
55
+ const raw = clean(el)
56
+ return /^\\d+$/.test(raw) ? Number(raw) : 0
57
+ }
58
+ const expandReplyThreads = async (root) => {
59
+ if (!withReplies || !root) return
60
+ const clickedTexts = new Set()
61
+ for (let round = 0; round < 3; round++) {
62
+ const expanders = Array.from(root.querySelectorAll('button, [role="button"], span, div')).filter(el => {
63
+ if (!(el instanceof HTMLElement)) return false
64
+ const text = clean(el)
65
+ if (!text || text.length > 24) return false
66
+ if (!/(展开|更多回复|全部回复|查看.*回复|共\\d+条回复)/.test(text)) return false
67
+ if (clickedTexts.has(text)) return false
68
+ return true
69
+ })
70
+ if (!expanders.length) break
71
+ for (const el of expanders) {
72
+ const text = clean(el)
73
+ el.click()
74
+ clickedTexts.add(text)
75
+ await wait(300)
76
+ }
77
+ }
78
+ }
44
79
 
45
80
  const results = []
46
81
  const parents = document.querySelectorAll('.parent-comment')
@@ -50,13 +85,24 @@ cli({
50
85
 
51
86
  const author = clean(item.querySelector('.author-wrapper .name, .user-name'))
52
87
  const text = clean(item.querySelector('.content, .note-text'))
53
- // XHS shows text "赞" when likes = 0; only shows a number when > 0
54
- const likesRaw = clean(item.querySelector('.count'))
55
- const likes = /^\\d+$/.test(likesRaw) ? Number(likesRaw) : 0
88
+ const likes = parseLikes(item.querySelector('.count'))
56
89
  const time = clean(item.querySelector('.date, .time'))
57
90
 
58
91
  if (!text) continue
59
- results.push({ author, text, likes, time })
92
+ results.push({ author, text, likes, time, is_reply: false, reply_to: '' })
93
+
94
+ // Extract nested replies (楼中楼)
95
+ if (withReplies) {
96
+ await expandReplyThreads(p)
97
+ p.querySelectorAll('.reply-container .comment-item-sub, .sub-comment-list .comment-item').forEach(sub => {
98
+ const sAuthor = clean(sub.querySelector('.name, .user-name'))
99
+ const sText = clean(sub.querySelector('.content, .note-text'))
100
+ const sLikes = parseLikes(sub.querySelector('.count'))
101
+ const sTime = clean(sub.querySelector('.date, .time'))
102
+ if (!sText) return
103
+ results.push({ author: sAuthor, text: sText, likes: sLikes, time: sTime, is_reply: true, reply_to: author })
104
+ })
105
+ }
60
106
  }
61
107
 
62
108
  return { loginWall, results }
@@ -68,7 +114,20 @@ cli({
68
114
  if (data.loginWall) {
69
115
  throw new AuthRequiredError('www.xiaohongshu.com', 'Note comments require login');
70
116
  }
71
- const results = data.results ?? [];
72
- return results.slice(0, limit).map((c, i) => ({ rank: i + 1, ...c }));
117
+ const all = data.results ?? [];
118
+ // When limiting, count only top-level comments; their replies are included for free
119
+ if (withReplies) {
120
+ const limited = [];
121
+ let topCount = 0;
122
+ for (const c of all) {
123
+ if (!c.is_reply)
124
+ topCount++;
125
+ if (topCount > limit)
126
+ break;
127
+ limited.push(c);
128
+ }
129
+ return limited.map((c, i) => ({ rank: i + 1, ...c }));
130
+ }
131
+ return all.slice(0, limit).map((c, i) => ({ rank: i + 1, ...c }));
73
132
  },
74
133
  });
@@ -33,22 +33,20 @@ describe('xiaohongshu comments', () => {
33
33
  const page = createPageMock({
34
34
  loginWall: false,
35
35
  results: [
36
- { author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01' },
37
- { author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02' },
36
+ { author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01', is_reply: false, reply_to: '' },
37
+ { author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02', is_reply: false, reply_to: '' },
38
38
  ],
39
39
  });
40
40
  const result = (await command.func(page, { 'note-id': '69aadbcb000000002202f131', limit: 5 }));
41
41
  expect(page.goto.mock.calls[0][0]).toContain('/explore/69aadbcb000000002202f131');
42
- expect(result).toEqual([
43
- { rank: 1, author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01' },
44
- { rank: 2, author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02' },
45
- ]);
46
- expect(result[0]).not.toHaveProperty('loginWall');
42
+ expect(result).toHaveLength(2);
43
+ expect(result[0]).toMatchObject({ rank: 1, author: 'Alice', text: 'Great note!', likes: 10 });
44
+ expect(result[1]).toMatchObject({ rank: 2, author: 'Bob', text: 'Very helpful', likes: 0 });
47
45
  });
48
46
  it('strips /explore/ prefix from full URL input', async () => {
49
47
  const page = createPageMock({
50
48
  loginWall: false,
51
- results: [{ author: 'Alice', text: 'Nice', likes: 1, time: '2024-01-01' }],
49
+ results: [{ author: 'Alice', text: 'Nice', likes: 1, time: '2024-01-01', is_reply: false, reply_to: '' }],
52
50
  });
53
51
  await command.func(page, {
54
52
  'note-id': 'https://www.xiaohongshu.com/explore/69aadbcb000000002202f131',
@@ -56,6 +54,15 @@ describe('xiaohongshu comments', () => {
56
54
  });
57
55
  expect(page.goto.mock.calls[0][0]).toContain('/explore/69aadbcb000000002202f131');
58
56
  });
57
+ it('preserves full search_result URL with xsec_token for navigation', async () => {
58
+ const page = createPageMock({
59
+ loginWall: false,
60
+ results: [{ author: 'Alice', text: 'Nice', likes: 1, time: '2024-01-01', is_reply: false, reply_to: '' }],
61
+ });
62
+ const fullUrl = 'https://www.xiaohongshu.com/search_result/69aadbcb000000002202f131?xsec_token=abc&xsec_source=pc_search';
63
+ await command.func(page, { 'note-id': fullUrl, limit: 5 });
64
+ expect(page.goto.mock.calls[0][0]).toBe(fullUrl);
65
+ });
59
66
  it('throws AuthRequiredError when login wall is detected', async () => {
60
67
  const page = createPageMock({ loginWall: true, results: [] });
61
68
  await expect(command.func(page, { 'note-id': 'abc123', limit: 5 })).rejects.toThrow('Note comments require login');
@@ -64,12 +71,14 @@ describe('xiaohongshu comments', () => {
64
71
  const page = createPageMock({ loginWall: false, results: [] });
65
72
  await expect(command.func(page, { 'note-id': 'abc123', limit: 5 })).resolves.toEqual([]);
66
73
  });
67
- it('respects the limit', async () => {
74
+ it('respects the limit for top-level comments', async () => {
68
75
  const manyComments = Array.from({ length: 10 }, (_, i) => ({
69
76
  author: `User${i}`,
70
77
  text: `Comment ${i}`,
71
78
  likes: i,
72
79
  time: '2024-01-01',
80
+ is_reply: false,
81
+ reply_to: '',
73
82
  }));
74
83
  const page = createPageMock({ loginWall: false, results: manyComments });
75
84
  const result = (await command.func(page, { 'note-id': 'abc123', limit: 3 }));
@@ -77,4 +86,56 @@ describe('xiaohongshu comments', () => {
77
86
  expect(result[0].rank).toBe(1);
78
87
  expect(result[2].rank).toBe(3);
79
88
  });
89
+ it('clamps invalid negative limits to a safe minimum', async () => {
90
+ const page = createPageMock({
91
+ loginWall: false,
92
+ results: [
93
+ { author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01', is_reply: false, reply_to: '' },
94
+ { author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02', is_reply: false, reply_to: '' },
95
+ ],
96
+ });
97
+ const result = (await command.func(page, { 'note-id': 'abc123', limit: -3 }));
98
+ expect(result).toHaveLength(1);
99
+ expect(result[0]).toMatchObject({ rank: 1, author: 'Alice' });
100
+ });
101
+ describe('--with-replies', () => {
102
+ it('includes reply rows with is_reply=true and reply_to set', async () => {
103
+ const page = createPageMock({
104
+ loginWall: false,
105
+ results: [
106
+ { author: 'Alice', text: 'Main comment', likes: 10, time: '03-25', is_reply: false, reply_to: '' },
107
+ { author: 'Bob', text: 'Reply to Alice', likes: 3, time: '03-25', is_reply: true, reply_to: 'Alice' },
108
+ { author: 'Carol', text: 'Another top', likes: 5, time: '03-26', is_reply: false, reply_to: '' },
109
+ ],
110
+ });
111
+ const result = (await command.func(page, {
112
+ 'note-id': 'abc123', limit: 50, 'with-replies': true,
113
+ }));
114
+ expect(result).toHaveLength(3);
115
+ expect(result[0]).toMatchObject({ author: 'Alice', is_reply: false, reply_to: '' });
116
+ expect(result[1]).toMatchObject({ author: 'Bob', is_reply: true, reply_to: 'Alice' });
117
+ expect(result[2]).toMatchObject({ author: 'Carol', is_reply: false, reply_to: '' });
118
+ const script = page.evaluate.mock.calls[0][0];
119
+ expect(script).toContain('共\\d+条回复');
120
+ expect(script).toContain('el.click()');
121
+ });
122
+ it('limits by top-level count, keeping attached replies', async () => {
123
+ const page = createPageMock({
124
+ loginWall: false,
125
+ results: [
126
+ { author: 'A', text: 'Top 1', likes: 0, time: '', is_reply: false, reply_to: '' },
127
+ { author: 'A1', text: 'Reply 1', likes: 0, time: '', is_reply: true, reply_to: 'A' },
128
+ { author: 'A2', text: 'Reply 2', likes: 0, time: '', is_reply: true, reply_to: 'A' },
129
+ { author: 'B', text: 'Top 2', likes: 0, time: '', is_reply: false, reply_to: '' },
130
+ { author: 'C', text: 'Top 3', likes: 0, time: '', is_reply: false, reply_to: '' },
131
+ ],
132
+ });
133
+ // Limit to 2 top-level comments — should include A + 2 replies + B = 4 rows
134
+ const result = (await command.func(page, {
135
+ 'note-id': 'abc123', limit: 2, 'with-replies': true,
136
+ }));
137
+ expect(result).toHaveLength(4);
138
+ expect(result.map((r) => r.author)).toEqual(['A', 'A1', 'A2', 'B']);
139
+ });
140
+ });
80
141
  });
@@ -2,6 +2,9 @@
2
2
  * Xiaohongshu download — download images and videos from a note.
3
3
  *
4
4
  * Usage:
5
- * opencli xiaohongshu download --note_id abc123 --output ./xhs
5
+ * opencli xiaohongshu download <note-id-or-url> --output ./xhs
6
+ *
7
+ * Accepts a bare note ID, a full xiaohongshu.com URL (with xsec_token),
8
+ * or a short link (http://xhslink.com/...).
6
9
  */
7
10
  export {};
@@ -2,11 +2,15 @@
2
2
  * Xiaohongshu download — download images and videos from a note.
3
3
  *
4
4
  * Usage:
5
- * opencli xiaohongshu download --note_id abc123 --output ./xhs
5
+ * opencli xiaohongshu download <note-id-or-url> --output ./xhs
6
+ *
7
+ * Accepts a bare note ID, a full xiaohongshu.com URL (with xsec_token),
8
+ * or a short link (http://xhslink.com/...).
6
9
  */
7
10
  import { cli, Strategy } from '../../registry.js';
8
11
  import { formatCookieHeader } from '../../download/index.js';
9
12
  import { downloadMedia } from '../../download/media-download.js';
13
+ import { buildNoteUrl, parseNoteId } from './note-helpers.js';
10
14
  cli({
11
15
  site: 'xiaohongshu',
12
16
  name: 'download',
@@ -14,15 +18,15 @@ cli({
14
18
  domain: 'www.xiaohongshu.com',
15
19
  strategy: Strategy.COOKIE,
16
20
  args: [
17
- { name: 'note-id', positional: true, required: true, help: 'Note ID (from URL)' },
21
+ { name: 'note-id', positional: true, required: true, help: 'Note ID, full URL, or short link' },
18
22
  { name: 'output', default: './xiaohongshu-downloads', help: 'Output directory' },
19
23
  ],
20
24
  columns: ['index', 'type', 'status', 'size'],
21
25
  func: async (page, kwargs) => {
22
- const noteId = kwargs['note-id'];
26
+ const rawInput = String(kwargs['note-id']);
23
27
  const output = kwargs.output;
24
- // Navigate to note page
25
- await page.goto(`https://www.xiaohongshu.com/explore/${noteId}`);
28
+ const noteId = parseNoteId(rawInput);
29
+ await page.goto(buildNoteUrl(rawInput));
26
30
  // Extract note info and media URLs
27
31
  const data = await page.evaluate(`
28
32
  (() => {
@@ -32,6 +36,18 @@ cli({
32
36
  author: '',
33
37
  media: []
34
38
  };
39
+ const seenMedia = new Set();
40
+ const pushMedia = (type, url) => {
41
+ if (!url) return;
42
+ const key = type + ':' + url;
43
+ if (seenMedia.has(key)) return;
44
+ seenMedia.add(key);
45
+ result.media.push({ type, url });
46
+ };
47
+ const locationMatch = (location.pathname || '').match(/\\/(?:explore|note|search_result|discovery\\/item)\\/([a-f0-9]+)/i);
48
+ if (locationMatch) {
49
+ result.noteId = locationMatch[1];
50
+ }
35
51
 
36
52
  // Get title
37
53
  const titleEl = document.querySelector('.title, #detail-title, .note-content .title');
@@ -57,7 +73,6 @@ cli({
57
73
  document.querySelectorAll(selector).forEach(img => {
58
74
  let src = img.src || img.getAttribute('data-src') || '';
59
75
  if (src && (src.includes('xhscdn') || src.includes('xiaohongshu'))) {
60
- // Convert to high quality URL (remove resize parameters)
61
76
  src = src.split('?')[0];
62
77
  src = src.replace(/\\/imageView\\d+\\/\\d+\\/w\\/\\d+/, '');
63
78
  imageUrls.add(src);
@@ -65,26 +80,69 @@ cli({
65
80
  });
66
81
  }
67
82
 
68
- // Get video if exists
69
- const videoSelectors = [
70
- 'video source',
71
- 'video[src]',
72
- '.player video',
73
- '.video-player video'
74
- ];
83
+ // Get video prefer real URL from page state over blob: URLs
75
84
 
76
- for (const selector of videoSelectors) {
77
- document.querySelectorAll(selector).forEach(v => {
78
- const src = v.src || v.getAttribute('src') || '';
79
- if (src) {
80
- result.media.push({ type: 'video', url: src });
85
+ // Method 1: Extract from __INITIAL_STATE__ (SSR hydration data)
86
+ try {
87
+ const state = window.__INITIAL_STATE__;
88
+ if (state) {
89
+ const noteData = state.note?.noteDetailMap || state.note?.note || {};
90
+ for (const key of Object.keys(noteData)) {
91
+ const note = noteData[key]?.note || noteData[key];
92
+ const video = note?.video;
93
+ if (video) {
94
+ const vUrl = video.url || video.originVideoKey || video.consumer?.originVideoKey;
95
+ if (vUrl) {
96
+ const fullUrl = vUrl.startsWith('http') ? vUrl : 'https://sns-video-bd.xhscdn.com/' + vUrl;
97
+ pushMedia('video', fullUrl);
98
+ }
99
+ const streams = video.media?.stream?.h264 || [];
100
+ for (const stream of streams) {
101
+ if (stream.masterUrl) pushMedia('video', stream.masterUrl);
102
+ }
103
+ }
81
104
  }
82
- });
105
+ }
106
+ } catch(e) {}
107
+
108
+ // Method 2: Extract video URLs from inline script JSON
109
+ if (result.media.filter(m => m.type === 'video').length === 0) {
110
+ try {
111
+ const scripts = document.querySelectorAll('script');
112
+ for (const s of scripts) {
113
+ const text = s.textContent || '';
114
+ const videoMatches = text.match(/https?:\\/\\/sns-video[^"'\\s]+\\.mp4[^"'\\s]*/g)
115
+ || text.match(/https?:\\/\\/[^"'\\s]*xhscdn[^"'\\s]*\\.mp4[^"'\\s]*/g);
116
+ if (videoMatches) {
117
+ videoMatches.forEach(url => {
118
+ pushMedia('video', url.replace(/\\\\u002F/g, '/'));
119
+ });
120
+ }
121
+ }
122
+ } catch(e) {}
123
+ }
124
+
125
+ // Method 3: Fallback to DOM video elements, skip blob: URLs
126
+ if (result.media.filter(m => m.type === 'video').length === 0) {
127
+ const videoSelectors = [
128
+ 'video source',
129
+ 'video[src]',
130
+ '.player video',
131
+ '.video-player video'
132
+ ];
133
+ for (const selector of videoSelectors) {
134
+ document.querySelectorAll(selector).forEach(v => {
135
+ const src = v.src || v.getAttribute('src') || '';
136
+ if (src && !src.startsWith('blob:')) {
137
+ pushMedia('video', src);
138
+ }
139
+ });
140
+ }
83
141
  }
84
142
 
85
143
  // Add images to media
86
144
  imageUrls.forEach(url => {
87
- result.media.push({ type: 'image', url: url });
145
+ pushMedia('image', url);
88
146
  });
89
147
 
90
148
  return result;
@@ -95,11 +153,14 @@ cli({
95
153
  }
96
154
  // Extract cookies for authenticated downloads
97
155
  const cookies = formatCookieHeader(await page.getCookies({ domain: 'xiaohongshu.com' }));
156
+ const resolvedNoteId = typeof data.noteId === 'string' && data.noteId.trim()
157
+ ? data.noteId.trim()
158
+ : noteId;
98
159
  return downloadMedia(data.media, {
99
160
  output,
100
- subdir: noteId,
161
+ subdir: resolvedNoteId,
101
162
  cookies,
102
- filenamePrefix: noteId,
163
+ filenamePrefix: resolvedNoteId,
103
164
  timeout: 60000,
104
165
  });
105
166
  },
@@ -0,0 +1 @@
1
+ import './download.js';
@@ -0,0 +1,75 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const { mockDownloadMedia, mockFormatCookieHeader } = vi.hoisted(() => ({
3
+ mockDownloadMedia: vi.fn(),
4
+ mockFormatCookieHeader: vi.fn(() => 'a=b'),
5
+ }));
6
+ vi.mock('../../download/media-download.js', () => ({
7
+ downloadMedia: mockDownloadMedia,
8
+ }));
9
+ vi.mock('../../download/index.js', () => ({
10
+ formatCookieHeader: mockFormatCookieHeader,
11
+ }));
12
+ import { getRegistry } from '../../registry.js';
13
+ import './download.js';
14
+ function createPageMock(evaluateResult) {
15
+ return {
16
+ goto: vi.fn().mockResolvedValue(undefined),
17
+ evaluate: vi.fn().mockResolvedValue(evaluateResult),
18
+ snapshot: vi.fn().mockResolvedValue(undefined),
19
+ click: vi.fn().mockResolvedValue(undefined),
20
+ typeText: vi.fn().mockResolvedValue(undefined),
21
+ pressKey: vi.fn().mockResolvedValue(undefined),
22
+ scrollTo: vi.fn().mockResolvedValue(undefined),
23
+ getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
24
+ wait: vi.fn().mockResolvedValue(undefined),
25
+ tabs: vi.fn().mockResolvedValue([]),
26
+ closeTab: vi.fn().mockResolvedValue(undefined),
27
+ newTab: vi.fn().mockResolvedValue(undefined),
28
+ selectTab: vi.fn().mockResolvedValue(undefined),
29
+ networkRequests: vi.fn().mockResolvedValue([]),
30
+ consoleMessages: vi.fn().mockResolvedValue([]),
31
+ scroll: vi.fn().mockResolvedValue(undefined),
32
+ autoScroll: vi.fn().mockResolvedValue(undefined),
33
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
34
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
35
+ getCookies: vi.fn().mockResolvedValue([{ name: 'sid', value: 'secret', domain: '.xiaohongshu.com' }]),
36
+ screenshot: vi.fn().mockResolvedValue(''),
37
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
38
+ };
39
+ }
40
+ describe('xiaohongshu download', () => {
41
+ const command = getRegistry().get('xiaohongshu/download');
42
+ beforeEach(() => {
43
+ mockDownloadMedia.mockReset();
44
+ mockFormatCookieHeader.mockClear();
45
+ mockDownloadMedia.mockResolvedValue([{ index: 1, type: 'video', status: 'success', size: '1 MB' }]);
46
+ });
47
+ it('preserves short links for navigation but uses canonical note id for output naming', async () => {
48
+ const page = createPageMock({
49
+ noteId: '69bc166f000000001a02069a',
50
+ media: [{ type: 'video', url: 'https://sns-video-hw.xhscdn.com/example.mp4' }],
51
+ });
52
+ const shortUrl = 'http://xhslink.com/o/4MKEjsZnhCz';
53
+ await command.func(page, { 'note-id': shortUrl, output: './out' });
54
+ expect(page.goto.mock.calls[0][0]).toBe(shortUrl);
55
+ expect(mockDownloadMedia).toHaveBeenCalledWith([{ type: 'video', url: 'https://sns-video-hw.xhscdn.com/example.mp4' }], expect.objectContaining({
56
+ output: './out',
57
+ subdir: '69bc166f000000001a02069a',
58
+ filenamePrefix: '69bc166f000000001a02069a',
59
+ cookies: 'a=b',
60
+ }));
61
+ });
62
+ it('preserves full note URL with xsec_token for navigation', async () => {
63
+ const page = createPageMock({
64
+ noteId: '69bc166f000000001a02069a',
65
+ media: [{ type: 'image', url: 'https://ci.xiaohongshu.com/example.jpg' }],
66
+ });
67
+ const fullUrl = 'https://www.xiaohongshu.com/explore/69bc166f000000001a02069a?xsec_token=abc&xsec_source=pc_search';
68
+ await command.func(page, { 'note-id': fullUrl, output: './out' });
69
+ expect(page.goto.mock.calls[0][0]).toBe(fullUrl);
70
+ expect(mockDownloadMedia).toHaveBeenCalledWith([{ type: 'image', url: 'https://ci.xiaohongshu.com/example.jpg' }], expect.objectContaining({
71
+ subdir: '69bc166f000000001a02069a',
72
+ filenamePrefix: '69bc166f000000001a02069a',
73
+ }));
74
+ });
75
+ });
@@ -0,0 +1,12 @@
1
+ /** Side-effect-free helpers shared by xiaohongshu note and comments commands. */
2
+ /** Extract a bare note ID from a full URL or raw ID string. */
3
+ export declare function parseNoteId(input: string): string;
4
+ /**
5
+ * Build the best navigation URL for a note.
6
+ *
7
+ * XHS blocks direct `/explore/<id>` access without a valid `xsec_token`.
8
+ * When the user passes a full URL (from search results), we preserve it
9
+ * so the browser navigates with the token intact. For bare IDs we fall
10
+ * back to the `/explore/<id>` path (works when cookies carry enough context).
11
+ */
12
+ export declare function buildNoteUrl(input: string): string;
@@ -0,0 +1,23 @@
1
+ /** Side-effect-free helpers shared by xiaohongshu note and comments commands. */
2
+ /** Extract a bare note ID from a full URL or raw ID string. */
3
+ export function parseNoteId(input) {
4
+ const trimmed = input.trim();
5
+ const match = trimmed.match(/\/(?:explore|note|search_result)\/([a-f0-9]+)/);
6
+ return match ? match[1] : trimmed;
7
+ }
8
+ /**
9
+ * Build the best navigation URL for a note.
10
+ *
11
+ * XHS blocks direct `/explore/<id>` access without a valid `xsec_token`.
12
+ * When the user passes a full URL (from search results), we preserve it
13
+ * so the browser navigates with the token intact. For bare IDs we fall
14
+ * back to the `/explore/<id>` path (works when cookies carry enough context).
15
+ */
16
+ export function buildNoteUrl(input) {
17
+ const trimmed = input.trim();
18
+ if (/^https?:\/\//.test(trimmed)) {
19
+ // Full URL — navigate as-is; the browser will follow any redirects
20
+ return trimmed;
21
+ }
22
+ return `https://www.xiaohongshu.com/explore/${trimmed}`;
23
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Xiaohongshu note — read full note content from a public note page.
3
+ *
4
+ * Extracts title, author, description text, and engagement metrics
5
+ * (likes, collects, comment count) via DOM extraction.
6
+ */
7
+ export {};
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Xiaohongshu note — read full note content from a public note page.
3
+ *
4
+ * Extracts title, author, description text, and engagement metrics
5
+ * (likes, collects, comment count) via DOM extraction.
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ import { AuthRequiredError, EmptyResultError } from '../../errors.js';
9
+ import { parseNoteId, buildNoteUrl } from './note-helpers.js';
10
+ cli({
11
+ site: 'xiaohongshu',
12
+ name: 'note',
13
+ description: '获取小红书笔记正文和互动数据',
14
+ domain: 'www.xiaohongshu.com',
15
+ strategy: Strategy.COOKIE,
16
+ args: [
17
+ { name: 'note-id', required: true, positional: true, help: 'Note ID or full URL (preserves xsec_token for access)' },
18
+ ],
19
+ columns: ['field', 'value'],
20
+ func: async (page, kwargs) => {
21
+ const raw = String(kwargs['note-id']);
22
+ const noteId = parseNoteId(raw);
23
+ const url = buildNoteUrl(raw);
24
+ await page.goto(url);
25
+ await page.wait(3);
26
+ const data = await page.evaluate(`
27
+ (() => {
28
+ const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
29
+ const notFound = /页面不见了|笔记不存在|无法浏览/.test(document.body.innerText || '')
30
+
31
+ const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
32
+
33
+ const title = clean(document.querySelector('#detail-title, .title'))
34
+ const desc = clean(document.querySelector('#detail-desc, .desc, .note-text'))
35
+ const author = clean(document.querySelector('.username, .author-wrapper .name'))
36
+ const likes = clean(document.querySelector('.like-wrapper .count'))
37
+ const collects = clean(document.querySelector('.collect-wrapper .count'))
38
+ const comments = clean(document.querySelector('.chat-wrapper .count'))
39
+
40
+ // Try to extract tags/topics
41
+ const tags = []
42
+ document.querySelectorAll('#detail-desc a.tag, #detail-desc a[href*="search_result"]').forEach(el => {
43
+ const t = (el.textContent || '').trim()
44
+ if (t) tags.push(t)
45
+ })
46
+
47
+ return { loginWall, notFound, title, desc, author, likes, collects, comments, tags }
48
+ })()
49
+ `);
50
+ if (!data || typeof data !== 'object') {
51
+ throw new EmptyResultError('xiaohongshu/note', 'Unexpected evaluate response');
52
+ }
53
+ if (data.loginWall) {
54
+ throw new AuthRequiredError('www.xiaohongshu.com', 'Note content requires login');
55
+ }
56
+ if (data.notFound) {
57
+ throw new EmptyResultError('xiaohongshu/note', `Note ${noteId} not found or unavailable — it may have been deleted or restricted`);
58
+ }
59
+ const d = data;
60
+ // XHS renders placeholder text like "赞"/"收藏"/"评论" when count is 0;
61
+ // normalize to '0' unless the value looks numeric.
62
+ const numOrZero = (v) => /^\d+/.test(v) ? v : '0';
63
+ const rows = [
64
+ { field: 'title', value: d.title || '' },
65
+ { field: 'author', value: d.author || '' },
66
+ { field: 'content', value: d.desc || '' },
67
+ { field: 'likes', value: numOrZero(d.likes || '') },
68
+ { field: 'collects', value: numOrZero(d.collects || '') },
69
+ { field: 'comments', value: numOrZero(d.comments || '') },
70
+ ];
71
+ if (d.tags?.length) {
72
+ rows.push({ field: 'tags', value: d.tags.join(', ') });
73
+ }
74
+ return rows;
75
+ },
76
+ });
@@ -0,0 +1 @@
1
+ import './note.js';