@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
@@ -0,0 +1,164 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { IPage } from '../../types.js';
3
+ import { getRegistry } from '../../registry.js';
4
+ import { parseNoteId, buildNoteUrl } from './note-helpers.js';
5
+ import './note.js';
6
+
7
+ function createPageMock(evaluateResult: any): IPage {
8
+ return {
9
+ goto: vi.fn().mockResolvedValue(undefined),
10
+ evaluate: vi.fn().mockResolvedValue(evaluateResult),
11
+ snapshot: vi.fn().mockResolvedValue(undefined),
12
+ click: vi.fn().mockResolvedValue(undefined),
13
+ typeText: vi.fn().mockResolvedValue(undefined),
14
+ pressKey: vi.fn().mockResolvedValue(undefined),
15
+ scrollTo: vi.fn().mockResolvedValue(undefined),
16
+ getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
17
+ wait: vi.fn().mockResolvedValue(undefined),
18
+ tabs: vi.fn().mockResolvedValue([]),
19
+ closeTab: vi.fn().mockResolvedValue(undefined),
20
+ newTab: vi.fn().mockResolvedValue(undefined),
21
+ selectTab: vi.fn().mockResolvedValue(undefined),
22
+ networkRequests: vi.fn().mockResolvedValue([]),
23
+ consoleMessages: vi.fn().mockResolvedValue([]),
24
+ scroll: vi.fn().mockResolvedValue(undefined),
25
+ autoScroll: vi.fn().mockResolvedValue(undefined),
26
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
27
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
28
+ getCookies: vi.fn().mockResolvedValue([]),
29
+ screenshot: vi.fn().mockResolvedValue(''),
30
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
31
+ };
32
+ }
33
+
34
+ describe('parseNoteId', () => {
35
+ it('extracts ID from /explore/ URL', () => {
36
+ expect(parseNoteId('https://www.xiaohongshu.com/explore/69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
37
+ });
38
+
39
+ it('extracts ID from /search_result/ URL with query params', () => {
40
+ expect(parseNoteId('https://www.xiaohongshu.com/search_result/69c131c9000000002800be4c?xsec_token=abc')).toBe('69c131c9000000002800be4c');
41
+ });
42
+
43
+ it('extracts ID from /note/ URL', () => {
44
+ expect(parseNoteId('https://www.xiaohongshu.com/note/69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
45
+ });
46
+
47
+ it('returns raw string when no URL pattern matches', () => {
48
+ expect(parseNoteId('69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
49
+ });
50
+
51
+ it('trims whitespace', () => {
52
+ expect(parseNoteId(' 69c131c9000000002800be4c ')).toBe('69c131c9000000002800be4c');
53
+ });
54
+ });
55
+
56
+ describe('buildNoteUrl', () => {
57
+ it('returns full URL as-is when given https URL', () => {
58
+ const url = 'https://www.xiaohongshu.com/search_result/abc123?xsec_token=tok';
59
+ expect(buildNoteUrl(url)).toBe(url);
60
+ });
61
+
62
+ it('constructs /explore/ URL for bare note ID', () => {
63
+ expect(buildNoteUrl('abc123')).toBe('https://www.xiaohongshu.com/explore/abc123');
64
+ });
65
+ });
66
+
67
+ describe('xiaohongshu note', () => {
68
+ const command = getRegistry().get('xiaohongshu/note');
69
+
70
+ it('is registered', () => {
71
+ expect(command).toBeDefined();
72
+ expect(command!.func).toBeTypeOf('function');
73
+ });
74
+
75
+ it('returns note content as field/value rows', async () => {
76
+ const page = createPageMock({
77
+ loginWall: false,
78
+ notFound: false,
79
+ title: '尚界Z7实车体验',
80
+ desc: '今天去看了实车,外观很帅',
81
+ author: '小红薯用户',
82
+ likes: '257',
83
+ collects: '98',
84
+ comments: '45',
85
+ tags: ['#尚界Z7', '#鸿蒙智行'],
86
+ });
87
+
88
+ const result = (await command!.func!(page, { 'note-id': '69c131c9000000002800be4c' })) as any[];
89
+
90
+ expect((page.goto as any).mock.calls[0][0]).toContain('/explore/69c131c9000000002800be4c');
91
+ expect(result).toEqual([
92
+ { field: 'title', value: '尚界Z7实车体验' },
93
+ { field: 'author', value: '小红薯用户' },
94
+ { field: 'content', value: '今天去看了实车,外观很帅' },
95
+ { field: 'likes', value: '257' },
96
+ { field: 'collects', value: '98' },
97
+ { field: 'comments', value: '45' },
98
+ { field: 'tags', value: '#尚界Z7, #鸿蒙智行' },
99
+ ]);
100
+ });
101
+
102
+ it('parses note ID from full /explore/ URL', async () => {
103
+ const page = createPageMock({
104
+ loginWall: false, notFound: false,
105
+ title: 'Test', desc: '', author: '', likes: '0', collects: '0', comments: '0', tags: [],
106
+ });
107
+
108
+ await command!.func!(page, {
109
+ 'note-id': 'https://www.xiaohongshu.com/explore/69c131c9000000002800be4c?xsec_token=abc',
110
+ });
111
+
112
+ expect((page.goto as any).mock.calls[0][0]).toContain('/explore/69c131c9000000002800be4c');
113
+ });
114
+
115
+ it('preserves full search_result URL with xsec_token for navigation', async () => {
116
+ const page = createPageMock({
117
+ loginWall: false, notFound: false,
118
+ title: 'Test', desc: '', author: '', likes: '0', collects: '0', comments: '0', tags: [],
119
+ });
120
+
121
+ const fullUrl = 'https://www.xiaohongshu.com/search_result/69c131c9000000002800be4c?xsec_token=abc';
122
+ await command!.func!(page, { 'note-id': fullUrl });
123
+
124
+ // Should navigate to the full URL as-is, not strip the token
125
+ expect((page.goto as any).mock.calls[0][0]).toBe(fullUrl);
126
+ });
127
+
128
+ it('throws AuthRequiredError on login wall', async () => {
129
+ const page = createPageMock({ loginWall: true, notFound: false });
130
+
131
+ await expect(command!.func!(page, { 'note-id': 'abc123' })).rejects.toThrow('Note content requires login');
132
+ });
133
+
134
+ it('throws EmptyResultError when note is not found', async () => {
135
+ const page = createPageMock({ loginWall: false, notFound: true });
136
+
137
+ await expect(command!.func!(page, { 'note-id': 'abc123' })).rejects.toThrow('returned no data');
138
+ });
139
+
140
+ it('normalizes placeholder text to 0 for zero-count metrics', async () => {
141
+ const page = createPageMock({
142
+ loginWall: false, notFound: false,
143
+ title: 'New note', desc: 'Just posted', author: 'Author',
144
+ likes: '赞', collects: '收藏', comments: '评论', tags: [],
145
+ });
146
+
147
+ const result = (await command!.func!(page, { 'note-id': 'abc123' })) as any[];
148
+ expect(result.find((r: any) => r.field === 'likes')!.value).toBe('0');
149
+ expect(result.find((r: any) => r.field === 'collects')!.value).toBe('0');
150
+ expect(result.find((r: any) => r.field === 'comments')!.value).toBe('0');
151
+ });
152
+
153
+ it('omits tags row when no tags present', async () => {
154
+ const page = createPageMock({
155
+ loginWall: false, notFound: false,
156
+ title: 'No tags', desc: 'Content', author: 'Author',
157
+ likes: '1', collects: '2', comments: '3', tags: [],
158
+ });
159
+
160
+ const result = (await command!.func!(page, { 'note-id': 'abc123' })) as any[];
161
+ expect(result.find((r: any) => r.field === 'tags')).toBeUndefined();
162
+ expect(result).toHaveLength(6);
163
+ });
164
+ });
@@ -0,0 +1,86 @@
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
+
8
+ import { cli, Strategy } from '../../registry.js';
9
+ import { AuthRequiredError, EmptyResultError } from '../../errors.js';
10
+ import { parseNoteId, buildNoteUrl } from './note-helpers.js';
11
+
12
+ cli({
13
+ site: 'xiaohongshu',
14
+ name: 'note',
15
+ description: '获取小红书笔记正文和互动数据',
16
+ domain: 'www.xiaohongshu.com',
17
+ strategy: Strategy.COOKIE,
18
+ args: [
19
+ { name: 'note-id', required: true, positional: true, help: 'Note ID or full URL (preserves xsec_token for access)' },
20
+ ],
21
+ columns: ['field', 'value'],
22
+ func: async (page, kwargs) => {
23
+ const raw = String(kwargs['note-id']);
24
+ const noteId = parseNoteId(raw);
25
+ const url = buildNoteUrl(raw);
26
+
27
+ await page.goto(url);
28
+ await page.wait(3);
29
+
30
+ const data = await page.evaluate(`
31
+ (() => {
32
+ const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
33
+ const notFound = /页面不见了|笔记不存在|无法浏览/.test(document.body.innerText || '')
34
+
35
+ const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
36
+
37
+ const title = clean(document.querySelector('#detail-title, .title'))
38
+ const desc = clean(document.querySelector('#detail-desc, .desc, .note-text'))
39
+ const author = clean(document.querySelector('.username, .author-wrapper .name'))
40
+ const likes = clean(document.querySelector('.like-wrapper .count'))
41
+ const collects = clean(document.querySelector('.collect-wrapper .count'))
42
+ const comments = clean(document.querySelector('.chat-wrapper .count'))
43
+
44
+ // Try to extract tags/topics
45
+ const tags = []
46
+ document.querySelectorAll('#detail-desc a.tag, #detail-desc a[href*="search_result"]').forEach(el => {
47
+ const t = (el.textContent || '').trim()
48
+ if (t) tags.push(t)
49
+ })
50
+
51
+ return { loginWall, notFound, title, desc, author, likes, collects, comments, tags }
52
+ })()
53
+ `);
54
+
55
+ if (!data || typeof data !== 'object') {
56
+ throw new EmptyResultError('xiaohongshu/note', 'Unexpected evaluate response');
57
+ }
58
+
59
+ if ((data as any).loginWall) {
60
+ throw new AuthRequiredError('www.xiaohongshu.com', 'Note content requires login');
61
+ }
62
+
63
+ if ((data as any).notFound) {
64
+ throw new EmptyResultError('xiaohongshu/note', `Note ${noteId} not found or unavailable — it may have been deleted or restricted`);
65
+ }
66
+
67
+ const d = data as any;
68
+ // XHS renders placeholder text like "赞"/"收藏"/"评论" when count is 0;
69
+ // normalize to '0' unless the value looks numeric.
70
+ const numOrZero = (v: string) => /^\d+/.test(v) ? v : '0';
71
+ const rows = [
72
+ { field: 'title', value: d.title || '' },
73
+ { field: 'author', value: d.author || '' },
74
+ { field: 'content', value: d.desc || '' },
75
+ { field: 'likes', value: numOrZero(d.likes || '') },
76
+ { field: 'collects', value: numOrZero(d.collects || '') },
77
+ { field: 'comments', value: numOrZero(d.comments || '') },
78
+ ];
79
+
80
+ if (d.tags?.length) {
81
+ rows.push({ field: 'tags', value: d.tags.join(', ') });
82
+ }
83
+
84
+ return rows;
85
+ },
86
+ });
@@ -8,7 +8,7 @@ import { getRegistry } from '../../registry.js';
8
8
  import type { IPage } from '../../types.js';
9
9
  import './publish.js';
10
10
 
11
- function createPageMock(evaluateResults: any[]): IPage {
11
+ function createPageMock(evaluateResults: any[], overrides: Partial<IPage> = {}): IPage {
12
12
  const evaluate = vi.fn();
13
13
  for (const result of evaluateResults) {
14
14
  evaluate.mockResolvedValueOnce(result);
@@ -37,10 +37,88 @@ function createPageMock(evaluateResults: any[]): IPage {
37
37
  getCookies: vi.fn().mockResolvedValue([]),
38
38
  screenshot: vi.fn().mockResolvedValue(''),
39
39
  waitForCapture: vi.fn().mockResolvedValue(undefined),
40
+ ...overrides,
40
41
  };
41
42
  }
42
43
 
43
44
  describe('xiaohongshu publish', () => {
45
+ it('prefers CDP setFileInput upload when the page supports it', async () => {
46
+ const cmd = getRegistry().get('xiaohongshu/publish');
47
+ expect(cmd?.func).toBeTypeOf('function');
48
+
49
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
50
+ const imagePath = path.join(tempDir, 'demo.jpg');
51
+ fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
52
+
53
+ const setFileInput = vi.fn().mockResolvedValue(undefined);
54
+ const page = createPageMock([
55
+ 'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
56
+ { ok: true, target: '上传图文', text: '上传图文' },
57
+ { state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
58
+ 'input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]',
59
+ false,
60
+ true,
61
+ { ok: true, sel: 'input[maxlength="20"]' },
62
+ { ok: true, sel: '[contenteditable="true"][class*="content"]' },
63
+ true,
64
+ 'https://creator.xiaohongshu.com/publish/success',
65
+ '发布成功',
66
+ ], {
67
+ setFileInput,
68
+ });
69
+
70
+ const result = await cmd!.func!(page, {
71
+ title: 'CDP上传优先',
72
+ content: '优先走 setFileInput 主路径',
73
+ images: imagePath,
74
+ topics: '',
75
+ draft: false,
76
+ });
77
+
78
+ expect(setFileInput).toHaveBeenCalledWith(
79
+ [imagePath],
80
+ expect.stringContaining('input[type="file"][accept*="image"]'),
81
+ );
82
+ const evaluateCalls = (page.evaluate as any).mock.calls.map((args: any[]) => String(args[0]));
83
+ expect(evaluateCalls.some((code: string) => code.includes('atob(img.base64)'))).toBe(false);
84
+ expect(result).toEqual([
85
+ {
86
+ status: '✅ 发布成功',
87
+ detail: '"CDP上传优先" · 1张图片 · 发布成功',
88
+ },
89
+ ]);
90
+ });
91
+
92
+ it('fails fast when only a generic file input exists on the page', async () => {
93
+ const cmd = getRegistry().get('xiaohongshu/publish');
94
+ expect(cmd?.func).toBeTypeOf('function');
95
+
96
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
97
+ const imagePath = path.join(tempDir, 'demo.jpg');
98
+ fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
99
+
100
+ const setFileInput = vi.fn().mockResolvedValue(undefined);
101
+ const page = createPageMock([
102
+ 'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
103
+ { ok: true, target: '上传图文', text: '上传图文' },
104
+ { state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
105
+ null,
106
+ ], {
107
+ setFileInput,
108
+ });
109
+
110
+ await expect(cmd!.func!(page, {
111
+ title: '不要走泛化上传',
112
+ content: 'generic file input 应该直接报错',
113
+ images: imagePath,
114
+ topics: '',
115
+ draft: false,
116
+ })).rejects.toThrow('Image injection failed: No file input found on page');
117
+
118
+ expect(setFileInput).not.toHaveBeenCalled();
119
+ expect(page.screenshot).toHaveBeenCalledWith({ path: '/tmp/xhs_publish_upload_debug.png' });
120
+ });
121
+
44
122
  it('selects the image-text tab and publishes successfully', async () => {
45
123
  const cmd = getRegistry().get('xiaohongshu/publish');
46
124
  expect(cmd?.func).toBeTypeOf('function');
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Flow:
5
5
  * 1. Navigate to creator publish page
6
- * 2. Upload images via DataTransfer injection into the file input
6
+ * 2. Upload images via CDP DOM.setFileInputFiles (with base64 fallback)
7
7
  * 3. Fill title and body text
8
8
  * 4. Add topic hashtags
9
9
  * 5. Publish (or save as draft)
@@ -43,44 +43,98 @@ const TITLE_SELECTORS = [
43
43
  'input[maxlength]',
44
44
  ];
45
45
 
46
- type ImagePayload = { name: string; mimeType: string; base64: string };
46
+ const SUPPORTED_EXTENSIONS: Record<string, string> = {
47
+ '.jpg': 'image/jpeg',
48
+ '.jpeg': 'image/jpeg',
49
+ '.png': 'image/png',
50
+ '.gif': 'image/gif',
51
+ '.webp': 'image/webp',
52
+ };
47
53
 
48
54
  /**
49
- * Read a local image and return the name, MIME type, and base64 content.
50
- * Throws if the file does not exist or the extension is unsupported.
55
+ * Validate image paths: check existence and extension.
56
+ * Returns resolved absolute paths.
51
57
  */
52
- function readImageFile(filePath: string): ImagePayload {
53
- const absPath = path.resolve(filePath);
54
- if (!fs.existsSync(absPath)) throw new Error(`Image file not found: ${absPath}`);
55
- const ext = path.extname(absPath).toLowerCase();
56
- const mimeMap: Record<string, string> = {
57
- '.jpg': 'image/jpeg',
58
- '.jpeg': 'image/jpeg',
59
- '.png': 'image/png',
60
- '.gif': 'image/gif',
61
- '.webp': 'image/webp',
62
- };
63
- const mimeType = mimeMap[ext];
64
- if (!mimeType) throw new Error(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
65
- const base64 = fs.readFileSync(absPath).toString('base64');
66
- return { name: path.basename(absPath), mimeType, base64 };
58
+ function validateImagePaths(filePaths: string[]): string[] {
59
+ return filePaths.map((filePath) => {
60
+ const absPath = path.resolve(filePath);
61
+ if (!fs.existsSync(absPath)) throw new Error(`Image file not found: ${absPath}`);
62
+ const ext = path.extname(absPath).toLowerCase();
63
+ if (!SUPPORTED_EXTENSIONS[ext]) {
64
+ throw new Error(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
65
+ }
66
+ return absPath;
67
+ });
67
68
  }
68
69
 
70
+ /** CSS selector for image-accepting file inputs. */
71
+ const IMAGE_INPUT_SELECTOR = 'input[type="file"][accept*="image"],'
72
+ + 'input[type="file"][accept*=".jpg"],'
73
+ + 'input[type="file"][accept*=".jpeg"],'
74
+ + 'input[type="file"][accept*=".png"],'
75
+ + 'input[type="file"][accept*=".gif"],'
76
+ + 'input[type="file"][accept*=".webp"]';
77
+
69
78
  /**
70
- * Inject images into the page's file input using DataTransfer.
71
- * Converts base64 payloads to File objects in the browser context, then dispatches
72
- * a synthetic 'change' event on the input element.
79
+ * Upload images via CDP DOM.setFileInputFiles Chrome reads files directly
80
+ * from the local filesystem, avoiding base64 payload size limits.
73
81
  *
74
- * Returns { ok, count, error }.
82
+ * Falls back to the legacy base64 DataTransfer approach if the extension
83
+ * does not support set-file-input (e.g. older extension version).
75
84
  */
76
- async function injectImages(page: IPage, images: ImagePayload[]): Promise<{ ok: boolean; count: number; error?: string }> {
85
+ async function uploadImages(
86
+ page: IPage,
87
+ absPaths: string[],
88
+ ): Promise<{ ok: boolean; count: number; error?: string }> {
89
+ // ── Primary: CDP DOM.setFileInputFiles ──────────────────────────────
90
+ if (page.setFileInput) {
91
+ try {
92
+ // Find image-accepting file input on the page
93
+ const selector: string | null = await page.evaluate(`
94
+ (() => {
95
+ const sels = ${JSON.stringify(IMAGE_INPUT_SELECTOR)};
96
+ const el = document.querySelector(sels);
97
+ return el ? sels : null;
98
+ })()
99
+ `);
100
+ if (!selector) {
101
+ return { ok: false, count: 0, error: 'No file input found on page' };
102
+ }
103
+ await page.setFileInput(absPaths, selector);
104
+ return { ok: true, count: absPaths.length };
105
+ } catch (err) {
106
+ // If set-file-input action is not supported by extension, fall through to legacy
107
+ const msg = err instanceof Error ? err.message : String(err);
108
+ if (msg.includes('Unknown action') || msg.includes('not supported')) {
109
+ // Extension too old — fall through to legacy base64 method
110
+ } else {
111
+ return { ok: false, count: 0, error: msg };
112
+ }
113
+ }
114
+ }
115
+
116
+ // ── Fallback: legacy base64 DataTransfer injection ─────────────────
117
+ const images = absPaths.map((absPath) => {
118
+ const base64 = fs.readFileSync(absPath).toString('base64');
119
+ const ext = path.extname(absPath).toLowerCase();
120
+ return { name: path.basename(absPath), mimeType: SUPPORTED_EXTENSIONS[ext], base64 };
121
+ });
122
+
123
+ // Warn if total payload is large — this may fail with older extensions
124
+ const totalBytes = images.reduce((sum, img) => sum + img.base64.length, 0);
125
+ if (totalBytes > 500_000) {
126
+ console.warn(
127
+ `[warn] Total image payload is ${(totalBytes / 1024 / 1024).toFixed(1)}MB (base64). ` +
128
+ 'This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, ' +
129
+ 'or compress images before publishing.'
130
+ );
131
+ }
132
+
77
133
  const payload = JSON.stringify(images);
78
134
  return page.evaluate(`
79
135
  (async () => {
80
136
  const images = ${payload};
81
137
 
82
- // Only use image-capable file inputs. Do not fall back to a generic uploader,
83
- // otherwise we can accidentally feed images into the video upload flow.
84
138
  const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
85
139
  const input = inputs.find(el => {
86
140
  const accept = el.getAttribute('accept') || '';
@@ -346,8 +400,8 @@ cli({
346
400
  if (imagePaths.length > MAX_IMAGES)
347
401
  throw new Error(`Too many images: ${imagePaths.length} (max ${MAX_IMAGES})`);
348
402
 
349
- // Read images in Node.js context before navigating (fast-fail on bad paths)
350
- const imageData: ImagePayload[] = imagePaths.map(readImageFile);
403
+ // Validate image paths before navigating (fast-fail on bad paths / unsupported formats)
404
+ const absImagePaths = validateImagePaths(imagePaths);
351
405
 
352
406
  // ── Step 1: Navigate to publish page ──────────────────────────────────────
353
407
  await page.goto(PUBLISH_URL);
@@ -377,7 +431,7 @@ cli({
377
431
  }
378
432
 
379
433
  // ── Step 3: Upload images ──────────────────────────────────────────────────
380
- const upload = await injectImages(page, imageData);
434
+ const upload = await uploadImages(page, absImagePaths);
381
435
  if (!upload.ok) {
382
436
  await page.screenshot({ path: '/tmp/xhs_publish_upload_debug.png' });
383
437
  throw new Error(
@@ -532,7 +586,7 @@ cli({
532
586
  status: isSuccess ? `✅ ${verb}` : '⚠️ 操作完成,请在浏览器中确认',
533
587
  detail: [
534
588
  `"${title}"`,
535
- `${imageData.length}张图片`,
589
+ `${absImagePaths.length}张图片`,
536
590
  topics.length ? `话题: ${topics.join(' ')}` : '',
537
591
  successMsg || finalUrl || '',
538
592
  ]
@@ -41,15 +41,16 @@ describe('xiaohongshu search', () => {
41
41
  expect(cmd?.func).toBeTypeOf('function');
42
42
 
43
43
  const page = createPageMock([
44
- {
45
- loginWall: true,
46
- results: [],
47
- },
44
+ // First evaluate: early login-wall check (returns true)
45
+ true,
48
46
  ]);
49
47
 
50
48
  await expect(cmd!.func!(page, { query: '特斯拉', limit: 5 })).rejects.toThrow(
51
49
  'Xiaohongshu search results are blocked behind a login wall'
52
50
  );
51
+
52
+ // autoScroll must NOT be called when a login wall is detected early
53
+ expect(page.autoScroll).not.toHaveBeenCalled();
53
54
  });
54
55
 
55
56
  it('returns ranked results with search_result url and author_url preserved', async () => {
@@ -62,6 +63,9 @@ describe('xiaohongshu search', () => {
62
63
  'https://www.xiaohongshu.com/user/profile/635a9c720000000018028b40?xsec_token=user-token&xsec_source=pc_search';
63
64
 
64
65
  const page = createPageMock([
66
+ // First evaluate: early login-wall check (returns false → no wall)
67
+ false,
68
+ // Second evaluate: main DOM extraction
65
69
  {
66
70
  loginWall: false,
67
71
  results: [
@@ -99,6 +103,9 @@ describe('xiaohongshu search', () => {
99
103
  expect(cmd?.func).toBeTypeOf('function');
100
104
 
101
105
  const page = createPageMock([
106
+ // First evaluate: early login-wall check (returns false → no wall)
107
+ false,
108
+ // Second evaluate: main DOM extraction
102
109
  {
103
110
  loginWall: false,
104
111
  results: [
@@ -44,6 +44,19 @@ cli({
44
44
  );
45
45
  await page.wait(3);
46
46
 
47
+ // Early login-wall detection: XHS may show a login gate instead of
48
+ // results. Check *before* autoScroll to avoid crashing on a page
49
+ // that has no meaningful content to scroll through.
50
+ const loginCheck = await page.evaluate(`
51
+ (() => /登录后查看搜索结果/.test(document.body?.innerText || ''))()
52
+ `);
53
+ if (loginCheck) {
54
+ throw new AuthRequiredError(
55
+ 'www.xiaohongshu.com',
56
+ 'Xiaohongshu search results are blocked behind a login wall',
57
+ );
58
+ }
59
+
47
60
  // Scroll a couple of times to load more results
48
61
  await page.autoScroll({ times: 2 });
49
62
 
@@ -75,6 +75,7 @@ describe('extractXhsUserNotes', () => {
75
75
  title: 'First note',
76
76
  type: 'video',
77
77
  likes: '4.6万',
78
+ cover: '',
78
79
  url: 'https://www.xiaohongshu.com/user/profile/user-1/note-1?xsec_token=abc&xsec_source=pc_user',
79
80
  },
80
81
  {
@@ -82,11 +83,33 @@ describe('extractXhsUserNotes', () => {
82
83
  title: 'Second note',
83
84
  type: 'normal',
84
85
  likes: '42',
86
+ cover: '',
85
87
  url: 'https://www.xiaohongshu.com/user/profile/fallback-user/note-2',
86
88
  },
87
89
  ]);
88
90
  });
89
91
 
92
+ it('extracts cover urls with fallback priority urlDefault -> urlPre -> url', () => {
93
+ const rows = extractXhsUserNotes(
94
+ {
95
+ noteGroups: [
96
+ [
97
+ { noteCard: { noteId: 'cover-1', cover: { urlDefault: 'https://img.example/default.jpg', urlPre: 'https://img.example/pre.jpg', url: 'https://img.example/raw.jpg' } } },
98
+ { noteCard: { noteId: 'cover-2', cover: { urlPre: 'https://img.example/pre-only.jpg', url: 'https://img.example/raw-only.jpg' } } },
99
+ { noteCard: { noteId: 'cover-3', cover: { url: 'https://img.example/raw-fallback.jpg' } } },
100
+ ],
101
+ ],
102
+ },
103
+ 'fallback-user'
104
+ );
105
+
106
+ expect(rows.map(row => row.cover)).toEqual([
107
+ 'https://img.example/default.jpg',
108
+ 'https://img.example/pre-only.jpg',
109
+ 'https://img.example/raw-fallback.jpg',
110
+ ]);
111
+ });
112
+
90
113
  it('deduplicates repeated notes by note id', () => {
91
114
  const rows = extractXhsUserNotes(
92
115
  {
@@ -8,6 +8,7 @@ export interface XhsUserNoteRow {
8
8
  title: string;
9
9
  type: string;
10
10
  likes: string;
11
+ cover: string;
11
12
  url: string;
12
13
  }
13
14
 
@@ -72,11 +73,14 @@ export function extractXhsUserNotes(snapshot: XhsUserPageSnapshot, fallbackUserI
72
73
  const xsecToken = toCleanString(entry?.xsecToken ?? entry?.xsec_token ?? noteCard.xsecToken ?? noteCard.xsec_token);
73
74
  const likes = toCleanString(noteCard.interactInfo?.likedCount ?? noteCard.interact_info?.liked_count ?? 0) || '0';
74
75
 
76
+ const cover = toCleanString(noteCard.cover?.urlDefault ?? noteCard.cover?.urlPre ?? noteCard.cover?.url ?? '');
77
+
75
78
  rows.push({
76
79
  id: noteId,
77
80
  title: toCleanString(noteCard.displayTitle ?? noteCard.display_title ?? noteCard.title),
78
81
  type: toCleanString(noteCard.type),
79
82
  likes,
83
+ cover,
80
84
  url: buildXhsNoteUrl(userId || fallbackUserId, noteId, xsecToken),
81
85
  });
82
86
  }