@jackwener/opencli 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/.github/workflows/build-extension.yml +2 -6
  2. package/.github/workflows/ci.yml +21 -1
  3. package/README.md +35 -6
  4. package/README.zh-CN.md +12 -5
  5. package/SKILL.md +2 -0
  6. package/dist/browser/cdp.d.ts +2 -1
  7. package/dist/browser/cdp.js +5 -0
  8. package/dist/browser/discover.d.ts +4 -1
  9. package/dist/browser/discover.js +6 -2
  10. package/dist/browser/errors.d.ts +2 -2
  11. package/dist/browser/errors.js +4 -12
  12. package/dist/browser/mcp.d.ts +2 -1
  13. package/dist/browser/page.d.ts +3 -0
  14. package/dist/browser/page.js +24 -1
  15. package/dist/build-manifest.d.ts +2 -0
  16. package/dist/build-manifest.js +39 -14
  17. package/dist/build-manifest.test.js +21 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +2 -1
  20. package/dist/cli-manifest.json +1567 -108
  21. package/dist/cli.js +68 -6
  22. package/dist/clis/36kr/article.d.ts +1 -0
  23. package/dist/clis/36kr/article.js +62 -0
  24. package/dist/clis/36kr/hot.d.ts +3 -0
  25. package/dist/clis/36kr/hot.js +80 -0
  26. package/dist/clis/36kr/hot.test.d.ts +1 -0
  27. package/dist/clis/36kr/hot.test.js +15 -0
  28. package/dist/clis/36kr/news.d.ts +1 -0
  29. package/dist/clis/36kr/news.js +51 -0
  30. package/dist/clis/36kr/news.test.d.ts +1 -0
  31. package/dist/clis/36kr/news.test.js +85 -0
  32. package/dist/clis/36kr/search.d.ts +1 -0
  33. package/dist/clis/36kr/search.js +72 -0
  34. package/dist/clis/bilibili/comments.d.ts +5 -0
  35. package/dist/clis/bilibili/comments.js +40 -0
  36. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  37. package/dist/clis/bilibili/comments.test.js +82 -0
  38. package/dist/clis/bluesky/feeds.yaml +29 -0
  39. package/dist/clis/bluesky/followers.yaml +33 -0
  40. package/dist/clis/bluesky/following.yaml +33 -0
  41. package/dist/clis/bluesky/profile.yaml +27 -0
  42. package/dist/clis/bluesky/search.yaml +34 -0
  43. package/dist/clis/bluesky/starter-packs.yaml +34 -0
  44. package/dist/clis/bluesky/thread.yaml +32 -0
  45. package/dist/clis/bluesky/trending.yaml +27 -0
  46. package/dist/clis/bluesky/user.yaml +34 -0
  47. package/dist/clis/chatgpt/ask.js +29 -14
  48. package/dist/clis/chatgpt/ax.d.ts +6 -0
  49. package/dist/clis/chatgpt/ax.js +172 -1
  50. package/dist/clis/chatgpt/model.d.ts +1 -0
  51. package/dist/clis/chatgpt/model.js +24 -0
  52. package/dist/clis/chatgpt/send.js +12 -3
  53. package/dist/clis/douban/download.d.ts +1 -0
  54. package/dist/clis/douban/download.js +67 -0
  55. package/dist/clis/douban/download.test.d.ts +1 -0
  56. package/dist/clis/douban/download.test.js +170 -0
  57. package/dist/clis/douban/photos.d.ts +1 -0
  58. package/dist/clis/douban/photos.js +34 -0
  59. package/dist/clis/douban/utils.d.ts +25 -0
  60. package/dist/clis/douban/utils.js +190 -1
  61. package/dist/clis/douban/utils.test.d.ts +1 -0
  62. package/dist/clis/douban/utils.test.js +64 -0
  63. package/dist/clis/imdb/person.d.ts +1 -0
  64. package/dist/clis/imdb/person.js +203 -0
  65. package/dist/clis/imdb/reviews.d.ts +1 -0
  66. package/dist/clis/imdb/reviews.js +88 -0
  67. package/dist/clis/imdb/search.d.ts +1 -0
  68. package/dist/clis/imdb/search.js +161 -0
  69. package/dist/clis/imdb/title.d.ts +1 -0
  70. package/dist/clis/imdb/title.js +93 -0
  71. package/dist/clis/imdb/top.d.ts +1 -0
  72. package/dist/clis/imdb/top.js +53 -0
  73. package/dist/clis/imdb/trending.d.ts +1 -0
  74. package/dist/clis/imdb/trending.js +52 -0
  75. package/dist/clis/imdb/utils.d.ts +46 -0
  76. package/dist/clis/imdb/utils.js +285 -0
  77. package/dist/clis/imdb/utils.test.d.ts +1 -0
  78. package/dist/clis/imdb/utils.test.js +88 -0
  79. package/dist/clis/jd/item.d.ts +4 -0
  80. package/dist/clis/jd/item.js +16 -15
  81. package/dist/clis/jd/item.test.js +16 -1
  82. package/dist/clis/linux-do/categories.yaml +38 -9
  83. package/dist/clis/linux-do/category.d.ts +1 -0
  84. package/dist/clis/linux-do/category.js +36 -0
  85. package/dist/clis/linux-do/feed.d.ts +45 -0
  86. package/dist/clis/linux-do/feed.js +397 -0
  87. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  88. package/dist/clis/linux-do/feed.test.js +118 -0
  89. package/dist/clis/linux-do/hot.d.ts +1 -0
  90. package/dist/clis/linux-do/hot.js +25 -0
  91. package/dist/clis/linux-do/latest.d.ts +1 -0
  92. package/dist/clis/linux-do/latest.js +18 -0
  93. package/dist/clis/linux-do/tags.yaml +41 -0
  94. package/dist/clis/linux-do/topic.yaml +41 -3
  95. package/dist/clis/linux-do/user-posts.yaml +67 -0
  96. package/dist/clis/linux-do/user-topics.yaml +54 -0
  97. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  98. package/dist/clis/paperreview/commands.test.js +243 -0
  99. package/dist/clis/paperreview/feedback.d.ts +1 -0
  100. package/dist/clis/paperreview/feedback.js +52 -0
  101. package/dist/clis/paperreview/review.d.ts +1 -0
  102. package/dist/clis/paperreview/review.js +37 -0
  103. package/dist/clis/paperreview/submit.d.ts +1 -0
  104. package/dist/clis/paperreview/submit.js +85 -0
  105. package/dist/clis/paperreview/utils.d.ts +46 -0
  106. package/dist/clis/paperreview/utils.js +197 -0
  107. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  108. package/dist/clis/paperreview/utils.test.js +49 -0
  109. package/dist/clis/producthunt/browse.d.ts +1 -0
  110. package/dist/clis/producthunt/browse.js +99 -0
  111. package/dist/clis/producthunt/hot.d.ts +1 -0
  112. package/dist/clis/producthunt/hot.js +110 -0
  113. package/dist/clis/producthunt/posts.d.ts +1 -0
  114. package/dist/clis/producthunt/posts.js +28 -0
  115. package/dist/clis/producthunt/today.d.ts +1 -0
  116. package/dist/clis/producthunt/today.js +35 -0
  117. package/dist/clis/producthunt/utils.d.ts +29 -0
  118. package/dist/clis/producthunt/utils.js +99 -0
  119. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  120. package/dist/clis/producthunt/utils.test.js +64 -0
  121. package/dist/clis/twitter/article.js +4 -28
  122. package/dist/clis/twitter/likes.d.ts +24 -0
  123. package/dist/clis/twitter/likes.js +217 -0
  124. package/dist/clis/twitter/likes.test.d.ts +1 -0
  125. package/dist/clis/twitter/likes.test.js +85 -0
  126. package/dist/clis/twitter/profile.js +4 -28
  127. package/dist/clis/twitter/search.js +2 -1
  128. package/dist/clis/twitter/search.test.js +2 -0
  129. package/dist/clis/twitter/shared.d.ts +6 -0
  130. package/dist/clis/twitter/shared.js +35 -0
  131. package/dist/clis/twitter/timeline.js +2 -13
  132. package/dist/clis/twitter/trending.js +29 -61
  133. package/dist/clis/v2ex/hot.yaml +17 -3
  134. package/dist/clis/weixin/download.d.ts +17 -0
  135. package/dist/clis/weixin/download.js +88 -20
  136. package/dist/clis/weread/book.js +2 -2
  137. package/dist/clis/weread/commands.test.d.ts +3 -0
  138. package/dist/clis/weread/commands.test.js +43 -0
  139. package/dist/clis/weread/highlights.js +2 -2
  140. package/dist/clis/weread/notebooks.js +2 -2
  141. package/dist/clis/weread/notes.js +3 -3
  142. package/dist/clis/weread/shelf.js +2 -2
  143. package/dist/clis/weread/utils.d.ts +4 -4
  144. package/dist/clis/weread/utils.js +32 -14
  145. package/dist/clis/weread/utils.test.js +1 -28
  146. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  147. package/dist/clis/xiaohongshu/comments.js +74 -0
  148. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  149. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  150. package/dist/clis/xiaohongshu/publish.js +179 -47
  151. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  152. package/dist/clis/xiaohongshu/publish.test.js +131 -0
  153. package/dist/clis/xiaohongshu/search.d.ts +8 -1
  154. package/dist/clis/xiaohongshu/search.js +20 -1
  155. package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
  156. package/dist/clis/xiaohongshu/search.test.js +32 -1
  157. package/dist/commanderAdapter.d.ts +1 -0
  158. package/dist/commanderAdapter.js +176 -29
  159. package/dist/commanderAdapter.test.d.ts +1 -0
  160. package/dist/commanderAdapter.test.js +62 -0
  161. package/dist/daemon.js +17 -1
  162. package/dist/discovery.js +48 -42
  163. package/dist/doctor.d.ts +2 -2
  164. package/dist/doctor.js +11 -4
  165. package/dist/download/index.js +63 -51
  166. package/dist/download/index.test.js +17 -4
  167. package/dist/engine.test.js +42 -0
  168. package/dist/errors.d.ts +4 -2
  169. package/dist/errors.js +17 -34
  170. package/dist/execution.d.ts +1 -3
  171. package/dist/execution.js +66 -8
  172. package/dist/execution.test.d.ts +1 -0
  173. package/dist/execution.test.js +40 -0
  174. package/dist/external.js +6 -1
  175. package/dist/hooks.js +2 -0
  176. package/dist/main.js +6 -0
  177. package/dist/output.js +5 -1
  178. package/dist/pipeline/executor.js +3 -4
  179. package/dist/plugin-manifest.d.ts +70 -0
  180. package/dist/plugin-manifest.js +160 -0
  181. package/dist/plugin-manifest.test.d.ts +4 -0
  182. package/dist/plugin-manifest.test.js +179 -0
  183. package/dist/plugin-scaffold.d.ts +28 -0
  184. package/dist/plugin-scaffold.js +142 -0
  185. package/dist/plugin-scaffold.test.d.ts +4 -0
  186. package/dist/plugin-scaffold.test.js +83 -0
  187. package/dist/plugin.d.ts +82 -11
  188. package/dist/plugin.js +870 -84
  189. package/dist/plugin.test.js +1032 -17
  190. package/dist/registry.d.ts +4 -0
  191. package/dist/registry.js +2 -0
  192. package/dist/runtime-detect.d.ts +21 -0
  193. package/dist/runtime-detect.js +32 -0
  194. package/dist/runtime-detect.test.d.ts +1 -0
  195. package/dist/runtime-detect.test.js +27 -0
  196. package/dist/runtime.d.ts +1 -0
  197. package/dist/runtime.js +2 -2
  198. package/dist/serialization.d.ts +2 -0
  199. package/dist/serialization.js +6 -0
  200. package/dist/types.d.ts +3 -0
  201. package/dist/update-check.d.ts +22 -0
  202. package/dist/update-check.js +112 -0
  203. package/dist/weixin-download.test.d.ts +1 -0
  204. package/dist/weixin-download.test.js +30 -0
  205. package/dist/weread-private-api-regression.test.d.ts +1 -0
  206. package/dist/weread-private-api-regression.test.js +122 -0
  207. package/dist/yaml-schema.d.ts +3 -0
  208. package/dist/yaml-schema.js +18 -1
  209. package/docs/.vitepress/config.mts +4 -0
  210. package/docs/adapters/browser/36kr.md +47 -0
  211. package/docs/adapters/browser/bluesky.md +53 -0
  212. package/docs/adapters/browser/douban.md +14 -0
  213. package/docs/adapters/browser/imdb.md +47 -0
  214. package/docs/adapters/browser/jd.md +2 -2
  215. package/docs/adapters/browser/linux-do.md +181 -20
  216. package/docs/adapters/browser/paperreview.md +43 -0
  217. package/docs/adapters/browser/producthunt.md +49 -0
  218. package/docs/adapters/desktop/chatgpt.md +5 -0
  219. package/docs/adapters/index.md +6 -2
  220. package/docs/advanced/download.md +4 -0
  221. package/docs/advanced/rate-limiter-plugin.md +99 -0
  222. package/docs/guide/electron-app-cli.md +200 -0
  223. package/docs/guide/getting-started.md +1 -0
  224. package/docs/guide/plugins.md +97 -0
  225. package/docs/zh/guide/electron-app-cli.md +188 -0
  226. package/docs/zh/guide/getting-started.md +1 -0
  227. package/docs/zh/guide/plugins.md +65 -0
  228. package/extension/package.json +1 -0
  229. package/extension/scripts/package-release.mjs +179 -0
  230. package/extension/src/background.ts +2 -0
  231. package/package.json +4 -1
  232. package/scripts/postinstall.js +10 -0
  233. package/src/browser/cdp.ts +8 -1
  234. package/src/browser/discover.ts +8 -3
  235. package/src/browser/errors.ts +13 -14
  236. package/src/browser/mcp.ts +2 -1
  237. package/src/browser/page.ts +24 -1
  238. package/src/build-manifest.test.ts +23 -0
  239. package/src/build-manifest.ts +40 -15
  240. package/src/capabilityRouting.ts +2 -1
  241. package/src/cli.ts +69 -6
  242. package/src/clis/36kr/article.ts +69 -0
  243. package/src/clis/36kr/hot.test.ts +19 -0
  244. package/src/clis/36kr/hot.ts +100 -0
  245. package/src/clis/36kr/news.test.ts +90 -0
  246. package/src/clis/36kr/news.ts +54 -0
  247. package/src/clis/36kr/search.ts +78 -0
  248. package/src/clis/bilibili/comments.test.ts +102 -0
  249. package/src/clis/bilibili/comments.ts +44 -0
  250. package/src/clis/bluesky/feeds.yaml +29 -0
  251. package/src/clis/bluesky/followers.yaml +33 -0
  252. package/src/clis/bluesky/following.yaml +33 -0
  253. package/src/clis/bluesky/profile.yaml +27 -0
  254. package/src/clis/bluesky/search.yaml +34 -0
  255. package/src/clis/bluesky/starter-packs.yaml +34 -0
  256. package/src/clis/bluesky/thread.yaml +32 -0
  257. package/src/clis/bluesky/trending.yaml +27 -0
  258. package/src/clis/bluesky/user.yaml +34 -0
  259. package/src/clis/chatgpt/ask.ts +28 -14
  260. package/src/clis/chatgpt/ax.ts +180 -1
  261. package/src/clis/chatgpt/model.ts +27 -0
  262. package/src/clis/chatgpt/send.ts +16 -6
  263. package/src/clis/douban/download.test.ts +196 -0
  264. package/src/clis/douban/download.ts +78 -0
  265. package/src/clis/douban/photos.ts +36 -0
  266. package/src/clis/douban/utils.test.ts +97 -0
  267. package/src/clis/douban/utils.ts +232 -1
  268. package/src/clis/imdb/person.ts +232 -0
  269. package/src/clis/imdb/reviews.ts +111 -0
  270. package/src/clis/imdb/search.ts +179 -0
  271. package/src/clis/imdb/title.ts +121 -0
  272. package/src/clis/imdb/top.ts +67 -0
  273. package/src/clis/imdb/trending.ts +66 -0
  274. package/src/clis/imdb/utils.test.ts +117 -0
  275. package/src/clis/imdb/utils.ts +305 -0
  276. package/src/clis/jd/item.test.ts +18 -1
  277. package/src/clis/jd/item.ts +18 -15
  278. package/src/clis/linux-do/categories.yaml +38 -9
  279. package/src/clis/linux-do/category.ts +37 -0
  280. package/src/clis/linux-do/feed.test.ts +132 -0
  281. package/src/clis/linux-do/feed.ts +501 -0
  282. package/src/clis/linux-do/hot.ts +26 -0
  283. package/src/clis/linux-do/latest.ts +19 -0
  284. package/src/clis/linux-do/tags.yaml +41 -0
  285. package/src/clis/linux-do/topic.yaml +41 -3
  286. package/src/clis/linux-do/user-posts.yaml +67 -0
  287. package/src/clis/linux-do/user-topics.yaml +54 -0
  288. package/src/clis/paperreview/commands.test.ts +283 -0
  289. package/src/clis/paperreview/feedback.ts +64 -0
  290. package/src/clis/paperreview/review.ts +47 -0
  291. package/src/clis/paperreview/submit.ts +119 -0
  292. package/src/clis/paperreview/utils.test.ts +68 -0
  293. package/src/clis/paperreview/utils.ts +276 -0
  294. package/src/clis/producthunt/browse.ts +109 -0
  295. package/src/clis/producthunt/hot.ts +127 -0
  296. package/src/clis/producthunt/posts.ts +29 -0
  297. package/src/clis/producthunt/today.ts +37 -0
  298. package/src/clis/producthunt/utils.test.ts +72 -0
  299. package/src/clis/producthunt/utils.ts +122 -0
  300. package/src/clis/twitter/article.ts +5 -28
  301. package/src/clis/twitter/likes.test.ts +91 -0
  302. package/src/clis/twitter/likes.ts +256 -0
  303. package/src/clis/twitter/profile.ts +5 -28
  304. package/src/clis/twitter/search.test.ts +2 -0
  305. package/src/clis/twitter/search.ts +3 -1
  306. package/src/clis/twitter/shared.ts +45 -0
  307. package/src/clis/twitter/timeline.ts +2 -13
  308. package/src/clis/twitter/trending.ts +29 -77
  309. package/src/clis/v2ex/hot.yaml +17 -3
  310. package/src/clis/weixin/download.ts +114 -20
  311. package/src/clis/weread/book.ts +2 -2
  312. package/src/clis/weread/commands.test.ts +57 -0
  313. package/src/clis/weread/highlights.ts +2 -2
  314. package/src/clis/weread/notebooks.ts +2 -2
  315. package/src/clis/weread/notes.ts +3 -3
  316. package/src/clis/weread/shelf.ts +2 -2
  317. package/src/clis/weread/utils.test.ts +1 -32
  318. package/src/clis/weread/utils.ts +41 -16
  319. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  320. package/src/clis/xiaohongshu/comments.ts +81 -0
  321. package/src/clis/xiaohongshu/publish.test.ts +151 -0
  322. package/src/clis/xiaohongshu/publish.ts +206 -54
  323. package/src/clis/xiaohongshu/search.test.ts +39 -1
  324. package/src/clis/xiaohongshu/search.ts +19 -1
  325. package/src/commanderAdapter.test.ts +78 -0
  326. package/src/commanderAdapter.ts +188 -24
  327. package/src/daemon.ts +19 -1
  328. package/src/discovery.ts +49 -48
  329. package/src/doctor.ts +15 -5
  330. package/src/download/index.test.ts +14 -4
  331. package/src/download/index.ts +67 -55
  332. package/src/engine.test.ts +38 -0
  333. package/src/errors.ts +26 -63
  334. package/src/execution.test.ts +47 -0
  335. package/src/execution.ts +67 -9
  336. package/src/external.ts +6 -1
  337. package/src/hooks.ts +1 -0
  338. package/src/main.ts +7 -0
  339. package/src/output.ts +3 -1
  340. package/src/pipeline/executor.ts +4 -6
  341. package/src/plugin-manifest.test.ts +223 -0
  342. package/src/plugin-manifest.ts +206 -0
  343. package/src/plugin-scaffold.test.ts +98 -0
  344. package/src/plugin-scaffold.ts +170 -0
  345. package/src/plugin.test.ts +1104 -17
  346. package/src/plugin.ts +1101 -86
  347. package/src/registry.ts +6 -1
  348. package/src/runtime-detect.test.ts +30 -0
  349. package/src/runtime-detect.ts +36 -0
  350. package/src/runtime.ts +3 -3
  351. package/src/serialization.ts +4 -0
  352. package/src/types.ts +3 -0
  353. package/src/update-check.ts +114 -0
  354. package/src/weixin-download.test.ts +64 -0
  355. package/src/weread-private-api-regression.test.ts +150 -0
  356. package/src/yaml-schema.ts +20 -0
  357. package/tests/e2e/browser-auth.test.ts +13 -9
  358. package/tests/e2e/browser-public-extended.test.ts +1 -1
  359. package/tests/e2e/browser-public.test.ts +62 -4
  360. package/tests/e2e/helpers.ts +2 -1
  361. package/tests/e2e/public-commands.test.ts +37 -3
  362. package/tests/smoke/api-health.test.ts +1 -1
  363. package/vitest.config.ts +10 -0
  364. package/dist/clis/linux-do/category.yaml +0 -51
  365. package/dist/clis/linux-do/hot.yaml +0 -50
  366. package/dist/clis/linux-do/latest.yaml +0 -40
  367. package/src/clis/linux-do/category.yaml +0 -51
  368. package/src/clis/linux-do/hot.yaml +0 -50
  369. package/src/clis/linux-do/latest.yaml +0 -40
@@ -27,6 +27,22 @@ const MAX_IMAGES = 9;
27
27
  const MAX_TITLE_LEN = 20;
28
28
  const UPLOAD_SETTLE_MS = 3000;
29
29
 
30
+ /** Selectors for the title field, ordered by priority (new UI first). */
31
+ const TITLE_SELECTORS = [
32
+ // New creator center (2026-03) uses contenteditable for the title field.
33
+ // Placeholder observed: "填写标题会有更多赞哦"
34
+ '[contenteditable="true"][placeholder*="标题"]',
35
+ '[contenteditable="true"][placeholder*="赞"]',
36
+ '[contenteditable="true"][class*="title"]',
37
+ 'input[maxlength="20"]',
38
+ 'input[class*="title"]',
39
+ 'input[placeholder*="标题"]',
40
+ 'input[placeholder*="title" i]',
41
+ '.title-input input',
42
+ '.note-title input',
43
+ 'input[maxlength]',
44
+ ];
45
+
30
46
  type ImagePayload = { name: string; mimeType: string; base64: string };
31
47
 
32
48
  /**
@@ -63,14 +79,22 @@ async function injectImages(page: IPage, images: ImagePayload[]): Promise<{ ok:
63
79
  (async () => {
64
80
  const images = ${payload};
65
81
 
66
- // Prefer image/* file inputs; fall back to the first available input.
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.
67
84
  const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
68
85
  const input = inputs.find(el => {
69
86
  const accept = el.getAttribute('accept') || '';
70
- return accept.includes('image') || accept.includes('.jpg') || accept.includes('.png');
71
- }) || inputs[0];
87
+ return (
88
+ accept.includes('image') ||
89
+ accept.includes('.jpg') ||
90
+ accept.includes('.jpeg') ||
91
+ accept.includes('.png') ||
92
+ accept.includes('.gif') ||
93
+ accept.includes('.webp')
94
+ );
95
+ });
72
96
 
73
- if (!input) return { ok: false, count: 0, error: 'No file input found on page' };
97
+ if (!input) return { ok: false, count: 0, error: 'No image file input found on page' };
74
98
 
75
99
  const dt = new DataTransfer();
76
100
  for (const img of images) {
@@ -151,6 +175,139 @@ async function fillField(page: IPage, selectors: string[], text: string, fieldNa
151
175
  }
152
176
  }
153
177
 
178
+ async function selectImageTextTab(
179
+ page: IPage,
180
+ ): Promise<{ ok: boolean; target?: string; text?: string; visibleTexts?: string[] }> {
181
+ const result = await page.evaluate(`
182
+ () => {
183
+ const isVisible = (el) => {
184
+ if (!el || el.offsetParent === null) return false;
185
+ const rect = el.getBoundingClientRect();
186
+ return rect.width > 0 && rect.height > 0;
187
+ };
188
+
189
+ const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
190
+ const selector = 'button, [role="tab"], [role="button"], a, label, div, span, li';
191
+ const nodes = Array.from(document.querySelectorAll(selector));
192
+ const targets = ['上传图文', '图文', '图片'];
193
+
194
+ for (const target of targets) {
195
+ for (const node of nodes) {
196
+ if (!isVisible(node)) continue;
197
+ const text = normalize(node.innerText || node.textContent || '');
198
+ if (!text || text.includes('视频')) continue;
199
+ if (text === target || text.startsWith(target) || text.includes(target)) {
200
+ const clickable = node.closest('button, [role="tab"], [role="button"], a, label') || node;
201
+ clickable.click();
202
+ return { ok: true, target, text };
203
+ }
204
+ }
205
+ }
206
+
207
+ const visibleTexts = [];
208
+ for (const node of nodes) {
209
+ if (!isVisible(node)) continue;
210
+ const text = normalize(node.innerText || node.textContent || '');
211
+ if (!text || text.length > 20) continue;
212
+ visibleTexts.push(text);
213
+ if (visibleTexts.length >= 20) break;
214
+ }
215
+ return { ok: false, visibleTexts };
216
+ }
217
+ `);
218
+ if (result?.ok) {
219
+ await page.wait({ time: 1 });
220
+ }
221
+ return result;
222
+ }
223
+
224
+ type PublishSurfaceState = 'video_surface' | 'image_surface' | 'editor_ready';
225
+
226
+ type PublishSurfaceInspection = {
227
+ state: PublishSurfaceState;
228
+ hasTitleInput: boolean;
229
+ hasImageInput: boolean;
230
+ hasVideoSurface: boolean;
231
+ };
232
+
233
+ async function inspectPublishSurfaceState(page: IPage): Promise<PublishSurfaceInspection> {
234
+ return page.evaluate(`
235
+ () => {
236
+ const text = (document.body?.innerText || '').replace(/\s+/g, ' ').trim();
237
+ const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
238
+ if (!el || el.offsetParent === null) return false;
239
+ const placeholder = (el.getAttribute('placeholder') || '').trim();
240
+ const cls = el.className ? String(el.className) : '';
241
+ const maxLength = Number(el.getAttribute('maxlength') || 0);
242
+ return (
243
+ placeholder.includes('标题') ||
244
+ /title/i.test(placeholder) ||
245
+ /title/i.test(cls) ||
246
+ maxLength === 20
247
+ );
248
+ });
249
+ const hasImageInput = !!Array.from(document.querySelectorAll('input[type="file"]')).find((el) => {
250
+ const accept = el.getAttribute('accept') || '';
251
+ return (
252
+ accept.includes('image') ||
253
+ accept.includes('.jpg') ||
254
+ accept.includes('.jpeg') ||
255
+ accept.includes('.png') ||
256
+ accept.includes('.gif') ||
257
+ accept.includes('.webp')
258
+ );
259
+ });
260
+ const hasVideoSurface = text.includes('拖拽视频到此处点击上传') || text.includes('上传视频');
261
+ const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
262
+ return { state, hasTitleInput, hasImageInput, hasVideoSurface };
263
+ }
264
+ `);
265
+ }
266
+
267
+ async function waitForPublishSurfaceState(
268
+ page: IPage,
269
+ maxWaitMs = 5_000,
270
+ ): Promise<PublishSurfaceInspection> {
271
+ const pollMs = 500;
272
+ const maxAttempts = Math.max(1, Math.ceil(maxWaitMs / pollMs));
273
+ let surface = await inspectPublishSurfaceState(page);
274
+
275
+ for (let i = 0; i < maxAttempts; i++) {
276
+ if (surface.state !== 'video_surface') {
277
+ return surface;
278
+ }
279
+ if (i < maxAttempts - 1) {
280
+ await page.wait({ time: pollMs / 1_000 });
281
+ surface = await inspectPublishSurfaceState(page);
282
+ }
283
+ }
284
+
285
+ return surface;
286
+ }
287
+
288
+ /**
289
+ * Poll until the title/content editing form appears on the page.
290
+ * The new creator center UI only renders the editor after images are uploaded.
291
+ */
292
+ async function waitForEditForm(page: IPage, maxWaitMs = 10_000): Promise<boolean> {
293
+ const pollMs = 1_000;
294
+ const maxAttempts = Math.ceil(maxWaitMs / pollMs);
295
+ for (let i = 0; i < maxAttempts; i++) {
296
+ const found: boolean = await page.evaluate(`
297
+ (() => {
298
+ const sels = ${JSON.stringify(TITLE_SELECTORS)};
299
+ for (const sel of sels) {
300
+ const el = document.querySelector(sel);
301
+ if (el && el.offsetParent !== null) return true;
302
+ }
303
+ return false;
304
+ })()`);
305
+ if (found) return true;
306
+ if (i < maxAttempts - 1) await page.wait({ time: pollMs / 1_000 });
307
+ }
308
+ return false;
309
+ }
310
+
154
311
  cli({
155
312
  site: 'xiaohongshu',
156
313
  name: 'publish',
@@ -161,7 +318,7 @@ cli({
161
318
  args: [
162
319
  { name: 'title', required: true, help: '笔记标题 (最多20字)' },
163
320
  { name: 'content', required: true, positional: true, help: '笔记正文' },
164
- { name: 'images', required: false, help: '图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)' },
321
+ { name: 'images', required: true, help: '图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)' },
165
322
  { name: 'topics', required: false, help: '话题标签,逗号分隔,不含 # 号' },
166
323
  { name: 'draft', type: 'bool', default: false, help: '保存为草稿,不直接发布' },
167
324
  ],
@@ -184,6 +341,8 @@ cli({
184
341
  if (title.length > MAX_TITLE_LEN)
185
342
  throw new Error(`Title is ${title.length} chars — must be ≤ ${MAX_TITLE_LEN}`);
186
343
  if (!content) throw new Error('Positional argument <content> is required');
344
+ if (imagePaths.length === 0)
345
+ throw new Error('At least one --images path is required. The creator center now requires images before showing the editor.');
187
346
  if (imagePaths.length > MAX_IMAGES)
188
347
  throw new Error(`Too many images: ${imagePaths.length} (max ${MAX_IMAGES})`);
189
348
 
@@ -204,51 +363,44 @@ cli({
204
363
  }
205
364
 
206
365
  // ── Step 2: Select 图文 (image+text) note type if tabs are present ─────────
207
- const tabClicked: boolean = await page.evaluate(`
208
- () => {
209
- const allEls = document.querySelectorAll('[class*="tab"], [class*="note-type"], [class*="type-item"]');
210
- for (const el of allEls) {
211
- const text = el.innerText || el.textContent || '';
212
- if ((text.includes('图文') || text.includes('图片')) && el.offsetParent !== null) {
213
- el.click();
214
- return true;
215
- }
216
- }
217
- return false;
218
- }
219
- `);
220
- if (tabClicked) await page.wait({ time: 1 });
366
+ const tabResult = await selectImageTextTab(page);
367
+ const surface = await waitForPublishSurfaceState(page, tabResult?.ok ? 5_000 : 2_000);
368
+ if (surface.state === 'video_surface') {
369
+ await page.screenshot({ path: '/tmp/xhs_publish_tab_debug.png' });
370
+ const detail = tabResult?.ok
371
+ ? `clicked "${tabResult.text}"`
372
+ : `visible candidates: ${(tabResult?.visibleTexts || []).join(' | ') || 'none'}`;
373
+ throw new Error(
374
+ 'Still on the video publish page after trying to select 图文. ' +
375
+ `Details: ${detail}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`
376
+ );
377
+ }
221
378
 
222
379
  // ── Step 3: Upload images ──────────────────────────────────────────────────
223
- if (imageData.length > 0) {
224
- const upload = await injectImages(page, imageData);
225
- if (!upload.ok) {
226
- await page.screenshot({ path: '/tmp/xhs_publish_upload_debug.png' });
227
- throw new Error(
228
- `Image injection failed: ${upload.error ?? 'unknown'}. ` +
229
- 'Debug screenshot: /tmp/xhs_publish_upload_debug.png'
230
- );
231
- }
232
- // Allow XHS to process and upload images to its CDN
233
- await page.wait({ time: UPLOAD_SETTLE_MS / 1_000 });
234
- await waitForUploads(page);
380
+ const upload = await injectImages(page, imageData);
381
+ if (!upload.ok) {
382
+ await page.screenshot({ path: '/tmp/xhs_publish_upload_debug.png' });
383
+ throw new Error(
384
+ `Image injection failed: ${upload.error ?? 'unknown'}. ` +
385
+ 'Debug screenshot: /tmp/xhs_publish_upload_debug.png'
386
+ );
387
+ }
388
+ // Allow XHS to process and upload images to its CDN
389
+ await page.wait({ time: UPLOAD_SETTLE_MS / 1_000 });
390
+ await waitForUploads(page);
391
+
392
+ // ── Step 3b: Wait for editor form to render ───────────────────────────────
393
+ const formReady = await waitForEditForm(page);
394
+ if (!formReady) {
395
+ await page.screenshot({ path: '/tmp/xhs_publish_form_debug.png' });
396
+ throw new Error(
397
+ 'Editing form did not appear after image upload. The page layout may have changed. ' +
398
+ 'Debug screenshot: /tmp/xhs_publish_form_debug.png'
399
+ );
235
400
  }
236
401
 
237
402
  // ── Step 4: Fill title ─────────────────────────────────────────────────────
238
- await fillField(
239
- page,
240
- [
241
- 'input[maxlength="20"]',
242
- 'input[class*="title"]',
243
- 'input[placeholder*="标题"]',
244
- 'input[placeholder*="title" i]',
245
- '.title-input input',
246
- '.note-title input',
247
- 'input[maxlength]',
248
- ],
249
- title,
250
- 'title'
251
- );
403
+ await fillField(page, TITLE_SELECTORS, title, 'title');
252
404
  await page.wait({ time: 0.5 });
253
405
 
254
406
  // ── Step 5: Fill content / body ────────────────────────────────────────────
@@ -263,7 +415,7 @@ cli({
263
415
  '.note-content [contenteditable="true"]',
264
416
  '.editor-content [contenteditable="true"]',
265
417
  // Broad fallback — last resort; filter out any title contenteditable
266
- '[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="title" i])',
418
+ '[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])',
267
419
  ],
268
420
  content,
269
421
  'content'
@@ -327,14 +479,14 @@ cli({
327
479
  }
328
480
 
329
481
  // ── Step 7: Publish or save draft ─────────────────────────────────────────
330
- const actionLabel = isDraft ? '存草稿' : '发布';
482
+ const actionLabels = isDraft ? ['暂存离开', '存草稿'] : ['发布', '发布笔记'];
331
483
  const btnClicked: boolean = await page.evaluate(`
332
- (label => {
484
+ (labels => {
333
485
  const buttons = document.querySelectorAll('button, [role="button"]');
334
486
  for (const btn of buttons) {
335
487
  const text = (btn.innerText || btn.textContent || '').trim();
336
488
  if (
337
- (text === label || text.includes(label) || text === '发布笔记') &&
489
+ labels.some(l => text === l || text.includes(l)) &&
338
490
  btn.offsetParent !== null &&
339
491
  !btn.disabled
340
492
  ) {
@@ -343,13 +495,13 @@ cli({
343
495
  }
344
496
  }
345
497
  return false;
346
- })(${JSON.stringify(actionLabel)})
498
+ })(${JSON.stringify(actionLabels)})
347
499
  `);
348
500
 
349
501
  if (!btnClicked) {
350
502
  await page.screenshot({ path: '/tmp/xhs_publish_submit_debug.png' });
351
503
  throw new Error(
352
- `Could not find "${actionLabel}" button. ` +
504
+ `Could not find "${actionLabels[0]}" button. ` +
353
505
  'Debug screenshot: /tmp/xhs_publish_submit_debug.png'
354
506
  );
355
507
  }
@@ -364,7 +516,7 @@ cli({
364
516
  const text = (el.innerText || '').trim();
365
517
  if (
366
518
  el.children.length === 0 &&
367
- (text.includes('发布成功') || text.includes('草稿已保存') || text.includes('上传成功'))
519
+ (text.includes('发布成功') || text.includes('草稿已保存') || text.includes('暂存成功') || text.includes('上传成功'))
368
520
  ) return text;
369
521
  }
370
522
  return '';
@@ -373,14 +525,14 @@ cli({
373
525
 
374
526
  const navigatedAway = !finalUrl.includes('/publish/publish');
375
527
  const isSuccess = successMsg.length > 0 || navigatedAway;
376
- const verb = isDraft ? '草稿已保存' : '发布成功';
528
+ const verb = isDraft ? '暂存成功' : '发布成功';
377
529
 
378
530
  return [
379
531
  {
380
532
  status: isSuccess ? `✅ ${verb}` : '⚠️ 操作完成,请在浏览器中确认',
381
533
  detail: [
382
534
  `"${title}"`,
383
- imageData.length ? `${imageData.length}张图片` : '无图',
535
+ `${imageData.length}张图片`,
384
536
  topics.length ? `话题: ${topics.join(' ')}` : '',
385
537
  successMsg || finalUrl || '',
386
538
  ]
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import type { IPage } from '../../types.js';
3
3
  import { getRegistry } from '../../registry.js';
4
- import './search.js';
4
+ import { noteIdToDate } from './search.js';
5
5
 
6
6
  function createPageMock(evaluateResults: any[]): IPage {
7
7
  const evaluate = vi.fn();
@@ -86,6 +86,7 @@ describe('xiaohongshu search', () => {
86
86
  title: '某鱼买FSD被坑了4万',
87
87
  author: '随风',
88
88
  likes: '261',
89
+ published_at: '2025-10-10',
89
90
  url: detailUrl,
90
91
  author_url: authorUrl,
91
92
  },
@@ -132,3 +133,40 @@ describe('xiaohongshu search', () => {
132
133
  expect(result[0]).toMatchObject({ rank: 1, title: 'Result A' });
133
134
  });
134
135
  });
136
+
137
+ describe('noteIdToDate (ObjectID timestamp parsing)', () => {
138
+ it('parses a known note ID to the correct China-timezone date', () => {
139
+ // 0x697f6c74 = 1769958516 → 2026-02-01 in UTC+8
140
+ expect(noteIdToDate('https://www.xiaohongshu.com/search_result/697f6c74000000002103de17')).toBe('2026-02-01');
141
+ // 0x68e90be8 → 2025-10-10 in UTC+8
142
+ expect(noteIdToDate('https://www.xiaohongshu.com/explore/68e90be80000000004022e66')).toBe('2025-10-10');
143
+ });
144
+
145
+ it('returns China date when UTC+8 crosses into the next day', () => {
146
+ // 0x69b739f0 = 2026-03-15 23:00 UTC = 2026-03-16 07:00 CST
147
+ // Without UTC+8 offset this would incorrectly return 2026-03-15
148
+ expect(noteIdToDate('https://www.xiaohongshu.com/search_result/69b739f00000000000000000')).toBe('2026-03-16');
149
+ });
150
+
151
+ it('handles /note/ path variant', () => {
152
+ expect(noteIdToDate('https://www.xiaohongshu.com/note/697f6c74000000002103de17')).toBe('2026-02-01');
153
+ });
154
+
155
+ it('handles URL with query parameters', () => {
156
+ expect(noteIdToDate('https://www.xiaohongshu.com/search_result/697f6c74000000002103de17?xsec_token=abc')).toBe('2026-02-01');
157
+ });
158
+
159
+ it('returns empty string for non-matching URLs', () => {
160
+ expect(noteIdToDate('https://www.xiaohongshu.com/user/profile/635a9c720000000018028b40')).toBe('');
161
+ expect(noteIdToDate('https://www.xiaohongshu.com/')).toBe('');
162
+ });
163
+
164
+ it('returns empty string for IDs shorter than 24 hex chars', () => {
165
+ expect(noteIdToDate('https://www.xiaohongshu.com/search_result/abcdef')).toBe('');
166
+ });
167
+
168
+ it('returns empty string when timestamp is out of range', () => {
169
+ // All zeros → ts = 0
170
+ expect(noteIdToDate('https://www.xiaohongshu.com/search_result/000000000000000000000000')).toBe('');
171
+ });
172
+ });
@@ -9,6 +9,23 @@
9
9
  import { cli, Strategy } from '../../registry.js';
10
10
  import { AuthRequiredError } from '../../errors.js';
11
11
 
12
+ /**
13
+ * Extract approximate publish date from a Xiaohongshu note URL.
14
+ * XHS note IDs follow MongoDB ObjectID format where the first 8 hex
15
+ * characters encode a Unix timestamp (the moment the ID was generated,
16
+ * which closely matches publish time but is not an official API field).
17
+ * e.g. "697f6c74..." → 0x697f6c74 = 1769958516 → 2026-02-01
18
+ */
19
+ export function noteIdToDate(url: string): string {
20
+ const match = url.match(/\/(?:search_result|explore|note)\/([0-9a-f]{24})(?=[?#/]|$)/i);
21
+ if (!match) return '';
22
+ const hex = match[1].substring(0, 8);
23
+ const ts = parseInt(hex, 16);
24
+ if (!ts || ts < 1_000_000_000 || ts > 4_000_000_000) return '';
25
+ // Offset by UTC+8 (China Standard Time) so the date matches what XHS users see
26
+ return new Date((ts + 8 * 3600) * 1000).toISOString().slice(0, 10);
27
+ }
28
+
12
29
  cli({
13
30
  site: 'xiaohongshu',
14
31
  name: 'search',
@@ -19,7 +36,7 @@ cli({
19
36
  { name: 'query', required: true, positional: true, help: 'Search keyword' },
20
37
  { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
21
38
  ],
22
- columns: ['rank', 'title', 'author', 'likes', 'url'],
39
+ columns: ['rank', 'title', 'author', 'likes', 'published_at', 'url'],
23
40
  func: async (page, kwargs) => {
24
41
  const keyword = encodeURIComponent(kwargs.query);
25
42
  await page.goto(
@@ -97,6 +114,7 @@ cli({
97
114
  .map((item: any, i: number) => ({
98
115
  rank: i + 1,
99
116
  ...item,
117
+ published_at: noteIdToDate(item.url),
100
118
  }));
101
119
  },
102
120
  });
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Command } from 'commander';
3
+ import type { CliCommand } from './registry.js';
4
+
5
+ const { mockExecuteCommand, mockRenderOutput } = vi.hoisted(() => ({
6
+ mockExecuteCommand: vi.fn(),
7
+ mockRenderOutput: vi.fn(),
8
+ }));
9
+
10
+ vi.mock('./execution.js', () => ({
11
+ executeCommand: mockExecuteCommand,
12
+ }));
13
+
14
+ vi.mock('./output.js', () => ({
15
+ render: mockRenderOutput,
16
+ }));
17
+
18
+ import { registerCommandToProgram } from './commanderAdapter.js';
19
+
20
+ describe('commanderAdapter arg passing', () => {
21
+ const cmd: CliCommand = {
22
+ site: 'paperreview',
23
+ name: 'submit',
24
+ description: 'Submit a PDF',
25
+ browser: false,
26
+ args: [
27
+ { name: 'pdf', positional: true, required: true, help: 'Path to the paper PDF' },
28
+ { name: 'dry-run', type: 'bool', default: false, help: 'Validate only' },
29
+ { name: 'prepare-only', type: 'bool', default: false, help: 'Prepare only' },
30
+ ],
31
+ func: vi.fn(),
32
+ };
33
+
34
+ beforeEach(() => {
35
+ mockExecuteCommand.mockReset();
36
+ mockExecuteCommand.mockResolvedValue([]);
37
+ mockRenderOutput.mockReset();
38
+ delete process.env.OPENCLI_VERBOSE;
39
+ process.exitCode = undefined;
40
+ });
41
+
42
+ it('passes bool flag values through to executeCommand for coercion', async () => {
43
+ const program = new Command();
44
+ const siteCmd = program.command('paperreview');
45
+ registerCommandToProgram(siteCmd, cmd);
46
+
47
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'false']);
48
+
49
+ expect(mockExecuteCommand).toHaveBeenCalled();
50
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
51
+ expect(kwargs.pdf).toBe('./paper.pdf');
52
+ expect(kwargs).toHaveProperty('dry-run');
53
+ });
54
+
55
+ it('passes valueless bool flags as true to executeCommand', async () => {
56
+ const program = new Command();
57
+ const siteCmd = program.command('paperreview');
58
+ registerCommandToProgram(siteCmd, cmd);
59
+
60
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--prepare-only']);
61
+
62
+ expect(mockExecuteCommand).toHaveBeenCalled();
63
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
64
+ expect(kwargs.pdf).toBe('./paper.pdf');
65
+ expect(kwargs['prepare-only']).toBe(true);
66
+ });
67
+
68
+ it('rejects invalid bool values before calling executeCommand', async () => {
69
+ const program = new Command();
70
+ const siteCmd = program.command('paperreview');
71
+ registerCommandToProgram(siteCmd, cmd);
72
+
73
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'maybe']);
74
+
75
+ // normalizeArgValue validates bools eagerly; executeCommand should not be reached
76
+ expect(mockExecuteCommand).not.toHaveBeenCalled();
77
+ });
78
+ });