@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
@@ -0,0 +1,45 @@
1
+ import type { IPage } from '../../types.js';
2
+
3
+ const QUERY_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
4
+
5
+ export function sanitizeQueryId(resolved: unknown, fallbackId: string): string {
6
+ return typeof resolved === 'string' && QUERY_ID_PATTERN.test(resolved) ? resolved : fallbackId;
7
+ }
8
+
9
+ export async function resolveTwitterQueryId(
10
+ page: Pick<IPage, 'evaluate'>,
11
+ operationName: string,
12
+ fallbackId: string,
13
+ ): Promise<string> {
14
+ const resolved = await page.evaluate(`async () => {
15
+ const operationName = ${JSON.stringify(operationName)};
16
+ try {
17
+ const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
18
+ if (ghResp.ok) {
19
+ const data = await ghResp.json();
20
+ const entry = data?.[operationName];
21
+ if (entry && entry.queryId) return entry.queryId;
22
+ }
23
+ } catch {}
24
+ try {
25
+ const scripts = performance.getEntriesByType('resource')
26
+ .filter(r => r.name.includes('client-web') && r.name.endsWith('.js'))
27
+ .map(r => r.name);
28
+ for (const scriptUrl of scripts.slice(0, 15)) {
29
+ try {
30
+ const text = await (await fetch(scriptUrl)).text();
31
+ const re = new RegExp('queryId:"([A-Za-z0-9_-]+)"[^}]{0,200}operationName:"' + operationName + '"');
32
+ const match = text.match(re);
33
+ if (match) return match[1];
34
+ } catch {}
35
+ }
36
+ } catch {}
37
+ return null;
38
+ }`);
39
+
40
+ return sanitizeQueryId(resolved, fallbackId);
41
+ }
42
+
43
+ export const __test__ = {
44
+ sanitizeQueryId,
45
+ };
@@ -1,5 +1,6 @@
1
1
  import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
2
2
  import { cli, Strategy } from '../../registry.js';
3
+ import { resolveTwitterQueryId } from './shared.js';
3
4
 
4
5
  // ── Twitter GraphQL constants ──────────────────────────────────────────
5
6
 
@@ -198,19 +199,7 @@ cli({
198
199
  if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
199
200
 
200
201
  // Dynamically resolve queryId for the selected endpoint
201
- const resolved = await page.evaluate(`async () => {
202
- try {
203
- const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
204
- if (ghResp.ok) {
205
- const data = await ghResp.json();
206
- const entry = data['${endpoint}'];
207
- if (entry && entry.queryId) return entry.queryId;
208
- }
209
- } catch {}
210
- return null;
211
- }`);
212
- // Validate queryId format to prevent injection from untrusted upstream
213
- const queryId = typeof resolved === 'string' && /^[A-Za-z0-9_-]+$/.test(resolved) ? resolved : fallbackQueryId;
202
+ const queryId = await resolveTwitterQueryId(page, endpoint, fallbackQueryId);
214
203
 
215
204
  // Build auth headers
216
205
  const headers = JSON.stringify({
@@ -1,19 +1,6 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import { AuthRequiredError, EmptyResultError } from '../../errors.js';
3
3
 
4
- // ── Twitter GraphQL constants ──────────────────────────────────────────
5
-
6
- const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
7
-
8
- // ── Types ──────────────────────────────────────────────────────────────
9
-
10
- interface TrendItem {
11
- rank: number;
12
- topic: string;
13
- tweets: string;
14
- category: string;
15
- }
16
-
17
4
  // ── CLI definition ────────────────────────────────────────────────────
18
5
 
19
6
  cli({
@@ -34,79 +21,44 @@ cli({
34
21
  await page.goto('https://x.com/explore/tabs/trending');
35
22
  await page.wait(3);
36
23
 
37
- // Extract CSRF token to verify login
24
+ // Verify login via CSRF cookie
38
25
  const ct0 = await page.evaluate(`(() => {
39
26
  return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
40
27
  })()`);
41
28
  if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
42
29
 
43
- // Try legacy guide.json API first (faster than DOM scraping)
44
- let trends: TrendItem[] = [];
45
-
46
- const apiData = await page.evaluate(`(async () => {
47
- const ct0 = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || '';
48
- const r = await fetch('/i/api/2/guide.json?include_page_configuration=true', {
49
- credentials: 'include',
50
- headers: {
51
- 'x-twitter-active-user': 'yes',
52
- 'x-csrf-token': ct0,
53
- 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'
30
+ // Scrape trends from DOM (consistent with what the user sees on the page)
31
+ // DOM children: [0] rank + category, [1] topic, optional post count,
32
+ // and a caret menu button identified by [data-testid="caret"].
33
+ await page.wait(2);
34
+ const trends = await page.evaluate(`(() => {
35
+ const items = [];
36
+ const cells = document.querySelectorAll('[data-testid="trend"]');
37
+ cells.forEach((cell) => {
38
+ const text = cell.textContent || '';
39
+ if (text.includes('Promoted')) return;
40
+ const container = cell.querySelector(':scope > div');
41
+ if (!container) return;
42
+ const divs = container.children;
43
+ if (divs.length < 2) return;
44
+ const topic = divs[1].textContent.trim();
45
+ if (!topic) return;
46
+ const catText = divs[0].textContent.trim();
47
+ const category = catText.replace(/^\\d+\\s*/, '').replace(/^\\xB7\\s*/, '').trim();
48
+ // Find post count: skip rank, topic, and the caret menu button
49
+ let tweets = 'N/A';
50
+ for (let j = 2; j < divs.length; j++) {
51
+ if (divs[j].matches('[data-testid="caret"]') || divs[j].querySelector('[data-testid="caret"]')) continue;
52
+ const t = divs[j].textContent.trim();
53
+ if (t && /\\d/.test(t)) { tweets = t; break; }
54
54
  }
55
+ items.push({ rank: items.length + 1, topic, tweets, category });
55
56
  });
56
- return r.ok ? await r.json() : null;
57
+ return items;
57
58
  })()`);
58
59
 
59
- if (apiData) {
60
- const instructions = apiData?.timeline?.instructions || [];
61
- const entries = instructions.flatMap((inst: any) => inst?.addEntries?.entries || inst?.entries || []);
62
- const apiTrends = entries
63
- .filter((e: any) => e.content?.timelineModule)
64
- .flatMap((e: any) => e.content.timelineModule.items || [])
65
- .map((t: any) => t?.item?.content?.trend)
66
- .filter(Boolean);
67
-
68
- trends = apiTrends.map((t: any, i: number) => ({
69
- rank: i + 1,
70
- topic: t.name,
71
- tweets: t.tweetCount ? String(t.tweetCount) : 'N/A',
72
- category: t.trendMetadata?.domainContext || '',
73
- }));
74
- }
75
-
76
- // Fallback: scrape from the loaded DOM
77
- if (trends.length === 0) {
78
- await page.wait(2);
79
- const domTrends = await page.evaluate(`(() => {
80
- const items = [];
81
- const cells = document.querySelectorAll('[data-testid="trend"]');
82
- cells.forEach((cell) => {
83
- const text = cell.textContent || '';
84
- if (text.includes('Promoted')) return;
85
- const container = cell.querySelector(':scope > div');
86
- if (!container) return;
87
- const divs = container.children;
88
- // Structure: divs[0] = rank + category, divs[1] = topic name, divs[2] = extra
89
- const topicEl = divs.length >= 2 ? divs[1] : null;
90
- const topic = topicEl ? topicEl.textContent.trim() : '';
91
- const catEl = divs.length >= 1 ? divs[0] : null;
92
- const catText = catEl ? catEl.textContent.trim() : '';
93
- const category = catText.replace(/^\\d+\\s*/, '').replace(/^\\xB7\\s*/, '').trim();
94
- const extraEl = divs.length >= 3 ? divs[2] : null;
95
- const extra = extraEl ? extraEl.textContent.trim() : '';
96
- if (topic) {
97
- items.push({ rank: items.length + 1, topic, tweets: extra || 'N/A', category });
98
- }
99
- });
100
- return items;
101
- })()`);
102
-
103
- if (Array.isArray(domTrends) && domTrends.length > 0) {
104
- trends = domTrends;
105
- }
106
- }
107
-
108
- if (trends.length === 0) {
109
- throw new EmptyResultError('twitter trending', 'API may have changed or login may be required.');
60
+ if (!Array.isArray(trends) || trends.length === 0) {
61
+ throw new EmptyResultError('twitter trending', 'No trends found. The page structure may have changed.');
110
62
  }
111
63
 
112
64
  return trends.slice(0, limit);
@@ -3,7 +3,7 @@ name: hot
3
3
  description: V2EX 热门话题
4
4
  domain: www.v2ex.com
5
5
  strategy: public
6
- browser: false
6
+ browser: true
7
7
 
8
8
  args:
9
9
  limit:
@@ -12,8 +12,22 @@ args:
12
12
  description: Number of topics
13
13
 
14
14
  pipeline:
15
- - fetch:
16
- url: https://www.v2ex.com/api/topics/hot.json
15
+ - navigate: https://www.v2ex.com/
16
+
17
+ - evaluate: |
18
+ (async () => {
19
+ const response = await fetch('/api/topics/hot.json', {
20
+ credentials: 'include',
21
+ headers: {
22
+ accept: 'application/json, text/plain, */*',
23
+ 'x-requested-with': 'XMLHttpRequest',
24
+ },
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`V2EX hot API request failed: ${response.status}`);
28
+ }
29
+ return await response.json();
30
+ })()
17
31
 
18
32
  - map:
19
33
  rank: ${{ index + 1 }}
@@ -54,6 +54,113 @@ export function normalizeWechatUrl(raw: string): string {
54
54
  return s;
55
55
  }
56
56
 
57
+ /**
58
+ * Format a WeChat article timestamp as a UTC+8 datetime string.
59
+ * Accepts either Unix seconds or milliseconds.
60
+ */
61
+ export function formatWechatTimestamp(rawTimestamp: string): string {
62
+ const ts = Number.parseInt(rawTimestamp, 10);
63
+ if (!Number.isFinite(ts) || ts <= 0) return '';
64
+
65
+ const timestampMs = rawTimestamp.length === 13 ? ts : ts * 1000;
66
+ const d = new Date(timestampMs);
67
+ const pad = (n: number): string => String(n).padStart(2, '0');
68
+ const utc8 = new Date(d.getTime() + 8 * 3600 * 1000);
69
+
70
+ return (
71
+ `${utc8.getUTCFullYear()}-` +
72
+ `${pad(utc8.getUTCMonth() + 1)}-` +
73
+ `${pad(utc8.getUTCDate())} ` +
74
+ `${pad(utc8.getUTCHours())}:` +
75
+ `${pad(utc8.getUTCMinutes())}:` +
76
+ `${pad(utc8.getUTCSeconds())}`
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Extract the raw create_time value from supported WeChat inline script formats.
82
+ */
83
+ export function extractWechatCreateTimeValue(htmlStr: string): string {
84
+ const jsDecodeMatch = htmlStr.match(
85
+ /create_time\s*:\s*JsDecode\('([^']+)'\)(?=[\s,;}]|$)/,
86
+ );
87
+ if (jsDecodeMatch) return jsDecodeMatch[1];
88
+
89
+ const directValueMatch = htmlStr.match(
90
+ /create_time\s*[:=]\s*(?:"([^"]+)"|'([^']+)'|([0-9A-Za-z]+))(?=[\s,;}]|$)/,
91
+ );
92
+ if (!directValueMatch) return '';
93
+
94
+ return directValueMatch[1] || directValueMatch[2] || directValueMatch[3] || '';
95
+ }
96
+
97
+ /**
98
+ * Extract the publish time from DOM text first, then fall back to numeric create_time values.
99
+ */
100
+ export function extractWechatPublishTime(
101
+ publishTimeText: string | null | undefined,
102
+ htmlStr: string,
103
+ ): string {
104
+ const normalizedPublishTime = (publishTimeText || '').trim();
105
+ if (normalizedPublishTime) return normalizedPublishTime;
106
+
107
+ const rawCreateTime = extractWechatCreateTimeValue(htmlStr);
108
+ if (!/^\d{10}$|^\d{13}$/.test(rawCreateTime)) return '';
109
+
110
+ return formatWechatTimestamp(rawCreateTime);
111
+ }
112
+
113
+ /**
114
+ * Build a self-contained helper for execution inside page.evaluate().
115
+ */
116
+ export function buildExtractWechatPublishTimeJs(): string {
117
+ return `(${function extractWechatPublishTimeInPage(
118
+ publishTimeText: string | null | undefined,
119
+ htmlStr: string,
120
+ ) {
121
+ function formatWechatTimestamp(rawTimestamp: string) {
122
+ const ts = Number.parseInt(rawTimestamp, 10);
123
+ if (!Number.isFinite(ts) || ts <= 0) return '';
124
+
125
+ const timestampMs = rawTimestamp.length === 13 ? ts : ts * 1000;
126
+ const d = new Date(timestampMs);
127
+ const pad = (n: number) => String(n).padStart(2, '0');
128
+ const utc8 = new Date(d.getTime() + 8 * 3600 * 1000);
129
+
130
+ return (
131
+ `${utc8.getUTCFullYear()}-` +
132
+ `${pad(utc8.getUTCMonth() + 1)}-` +
133
+ `${pad(utc8.getUTCDate())} ` +
134
+ `${pad(utc8.getUTCHours())}:` +
135
+ `${pad(utc8.getUTCMinutes())}:` +
136
+ `${pad(utc8.getUTCSeconds())}`
137
+ );
138
+ }
139
+
140
+ function extractWechatCreateTimeValue(html: string) {
141
+ const jsDecodeMatch = html.match(
142
+ /create_time\s*:\s*JsDecode\('([^']+)'\)(?=[\s,;}]|$)/,
143
+ );
144
+ if (jsDecodeMatch) return jsDecodeMatch[1];
145
+
146
+ const directValueMatch = html.match(
147
+ /create_time\s*[:=]\s*(?:"([^"]+)"|'([^']+)'|([0-9A-Za-z]+))(?=[\s,;}]|$)/,
148
+ );
149
+ if (!directValueMatch) return '';
150
+
151
+ return directValueMatch[1] || directValueMatch[2] || directValueMatch[3] || '';
152
+ }
153
+
154
+ const normalizedPublishTime = (publishTimeText || '').trim();
155
+ if (normalizedPublishTime) return normalizedPublishTime;
156
+
157
+ const rawCreateTime = extractWechatCreateTimeValue(htmlStr);
158
+ if (!/^\d{10}$|^\d{13}$/.test(rawCreateTime)) return '';
159
+
160
+ return formatWechatTimestamp(rawCreateTime);
161
+ }.toString()})`;
162
+ }
163
+
57
164
  // ============================================================
58
165
  // CLI Registration
59
166
  // ============================================================
@@ -102,26 +209,13 @@ cli({
102
209
  const authorEl = document.querySelector('#js_name');
103
210
  result.author = authorEl ? authorEl.textContent.trim() : '';
104
211
 
105
- // Publish time: extract create_time from script tags
106
- const htmlStr = document.documentElement.innerHTML;
107
- let timeMatch = htmlStr.match(/create_time\\s*:\\s*JsDecode\\('([^']+)'\\)/);
108
- if (!timeMatch) timeMatch = htmlStr.match(/create_time\\s*:\\s*'(\\d+)'/);
109
- if (!timeMatch) timeMatch = htmlStr.match(/create_time\\s*[:=]\\s*["']?(\\d+)["']?/);
110
- if (timeMatch) {
111
- const ts = parseInt(timeMatch[1], 10);
112
- if (ts > 0) {
113
- const d = new Date(ts * 1000);
114
- const pad = n => String(n).padStart(2, '0');
115
- const utc8 = new Date(d.getTime() + 8 * 3600 * 1000);
116
- result.publishTime =
117
- utc8.getUTCFullYear() + '-' +
118
- pad(utc8.getUTCMonth() + 1) + '-' +
119
- pad(utc8.getUTCDate()) + ' ' +
120
- pad(utc8.getUTCHours()) + ':' +
121
- pad(utc8.getUTCMinutes()) + ':' +
122
- pad(utc8.getUTCSeconds());
123
- }
124
- }
212
+ // Publish time: prefer the rendered DOM text, then fall back to numeric create_time values.
213
+ const publishTimeEl = document.querySelector('#publish_time');
214
+ const extractWechatPublishTime = ${buildExtractWechatPublishTimeJs()};
215
+ result.publishTime = extractWechatPublishTime(
216
+ publishTimeEl ? publishTimeEl.textContent : '',
217
+ document.documentElement.innerHTML,
218
+ );
125
219
 
126
220
  // Content processing
127
221
  const contentEl = document.querySelector('#js_content');
@@ -1,6 +1,6 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import type { IPage } from '../../types.js';
3
- import { fetchWithPage } from './utils.js';
3
+ import { fetchPrivateApi } from './utils.js';
4
4
 
5
5
  cli({
6
6
  site: 'weread',
@@ -13,7 +13,7 @@ cli({
13
13
  ],
14
14
  columns: ['title', 'author', 'publisher', 'intro', 'category', 'rating'],
15
15
  func: async (page: IPage, args) => {
16
- const data = await fetchWithPage(page, '/book/info', { bookId: args.bookId });
16
+ const data = await fetchPrivateApi(page, '/book/info', { bookId: args['book-id'] });
17
17
  // newRating is 0-1000 scale per community docs; needs runtime verification
18
18
  const rating = data.newRating ? `${(data.newRating / 10).toFixed(1)}%` : '-';
19
19
  return [{
@@ -0,0 +1,57 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { mockFetchPrivateApi } = vi.hoisted(() => ({
4
+ mockFetchPrivateApi: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('./utils.js', async () => {
8
+ const actual = await vi.importActual<typeof import('./utils.js')>('./utils.js');
9
+ return {
10
+ ...actual,
11
+ fetchPrivateApi: mockFetchPrivateApi,
12
+ };
13
+ });
14
+
15
+ import { getRegistry } from '../../registry.js';
16
+ import './book.js';
17
+ import './highlights.js';
18
+ import './notes.js';
19
+
20
+ describe('weread book-id positional args', () => {
21
+ const book = getRegistry().get('weread/book');
22
+ const highlights = getRegistry().get('weread/highlights');
23
+ const notes = getRegistry().get('weread/notes');
24
+
25
+ beforeEach(() => {
26
+ mockFetchPrivateApi.mockReset();
27
+ });
28
+
29
+ it('passes the positional book-id to book details', async () => {
30
+ mockFetchPrivateApi.mockResolvedValue({ title: 'Three Body', newRating: 880 });
31
+
32
+ await book!.func!({} as any, { 'book-id': '12345' });
33
+
34
+ expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/book/info', { bookId: '12345' });
35
+ });
36
+
37
+ it('passes the positional book-id to highlights', async () => {
38
+ mockFetchPrivateApi.mockResolvedValue({ updated: [] });
39
+
40
+ await highlights!.func!({} as any, { 'book-id': 'abc', limit: 5 });
41
+
42
+ expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/book/bookmarklist', { bookId: 'abc' });
43
+ });
44
+
45
+ it('passes the positional book-id to notes', async () => {
46
+ mockFetchPrivateApi.mockResolvedValue({ reviews: [] });
47
+
48
+ await notes!.func!({} as any, { 'book-id': 'xyz', limit: 5 });
49
+
50
+ expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/review/list', {
51
+ bookId: 'xyz',
52
+ listType: '11',
53
+ mine: '1',
54
+ synckey: '0',
55
+ });
56
+ });
57
+ });
@@ -1,6 +1,6 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import type { IPage } from '../../types.js';
3
- import { fetchWithPage, formatDate } from './utils.js';
3
+ import { fetchPrivateApi, formatDate } from './utils.js';
4
4
 
5
5
  cli({
6
6
  site: 'weread',
@@ -14,7 +14,7 @@ cli({
14
14
  ],
15
15
  columns: ['chapter', 'text', 'createTime'],
16
16
  func: async (page: IPage, args) => {
17
- const data = await fetchWithPage(page, '/book/bookmarklist', { bookId: args.bookId });
17
+ const data = await fetchPrivateApi(page, '/book/bookmarklist', { bookId: args['book-id'] });
18
18
  const items: any[] = data?.updated ?? [];
19
19
  return items.slice(0, Number(args.limit)).map((item: any) => ({
20
20
  chapter: item.chapterName ?? '',
@@ -1,6 +1,6 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import type { IPage } from '../../types.js';
3
- import { fetchWithPage } from './utils.js';
3
+ import { fetchPrivateApi } from './utils.js';
4
4
 
5
5
  cli({
6
6
  site: 'weread',
@@ -10,7 +10,7 @@ cli({
10
10
  strategy: Strategy.COOKIE,
11
11
  columns: ['title', 'author', 'noteCount', 'bookId'],
12
12
  func: async (page: IPage, _args) => {
13
- const data = await fetchWithPage(page, '/user/notebooks');
13
+ const data = await fetchPrivateApi(page, '/user/notebooks');
14
14
  const books: any[] = data?.books ?? [];
15
15
  return books.map((item: any) => ({
16
16
  title: item.book?.title ?? '',
@@ -1,6 +1,6 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import type { IPage } from '../../types.js';
3
- import { fetchWithPage, formatDate } from './utils.js';
3
+ import { fetchPrivateApi, formatDate } from './utils.js';
4
4
 
5
5
  cli({
6
6
  site: 'weread',
@@ -14,8 +14,8 @@ cli({
14
14
  ],
15
15
  columns: ['chapter', 'text', 'review', 'createTime'],
16
16
  func: async (page: IPage, args) => {
17
- const data = await fetchWithPage(page, '/review/list', {
18
- bookId: args.bookId,
17
+ const data = await fetchPrivateApi(page, '/review/list', {
18
+ bookId: args['book-id'],
19
19
  listType: '11',
20
20
  mine: '1',
21
21
  synckey: '0',
@@ -1,6 +1,6 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import type { IPage } from '../../types.js';
3
- import { fetchWithPage } from './utils.js';
3
+ import { fetchPrivateApi } from './utils.js';
4
4
 
5
5
  cli({
6
6
  site: 'weread',
@@ -13,7 +13,7 @@ cli({
13
13
  ],
14
14
  columns: ['title', 'author', 'progress', 'bookId'],
15
15
  func: async (page: IPage, args) => {
16
- const data = await fetchWithPage(page, '/shelf/sync', { synckey: '0', lectureSynckey: '0' });
16
+ const data = await fetchPrivateApi(page, '/shelf/sync', { synckey: '0', lectureSynckey: '0' });
17
17
  const books: any[] = data?.books ?? [];
18
18
  return books.slice(0, Number(args.limit)).map((item: any) => ({
19
19
  title: item.bookInfo?.title ?? item.title ?? '',
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { formatDate, fetchWebApi, fetchWithPage } from './utils.js';
2
+ import { formatDate, fetchWebApi } from './utils.js';
3
3
 
4
4
  describe('formatDate', () => {
5
5
  it('formats a typical Unix timestamp in UTC+8', () => {
@@ -71,34 +71,3 @@ describe('fetchWebApi', () => {
71
71
  await expect(fetchWebApi('/search/global')).rejects.toThrow('Invalid JSON');
72
72
  });
73
73
  });
74
-
75
- describe('fetchWithPage', () => {
76
- it('throws AUTH_REQUIRED on errcode -2010', async () => {
77
- const mockPage = {
78
- evaluate: vi.fn().mockResolvedValue({ errcode: -2010, errmsg: '用户不存在' }),
79
- } as any;
80
- await expect(fetchWithPage(mockPage, '/book/info')).rejects.toThrow('Not logged in');
81
- });
82
-
83
- it('throws API_ERROR on unknown errcode', async () => {
84
- const mockPage = {
85
- evaluate: vi.fn().mockResolvedValue({ errcode: -1, errmsg: 'unknown error' }),
86
- } as any;
87
- await expect(fetchWithPage(mockPage, '/book/info')).rejects.toThrow('unknown error');
88
- });
89
-
90
- it('returns data on success (errcode 0 or absent)', async () => {
91
- const mockPage = {
92
- evaluate: vi.fn().mockResolvedValue({ title: 'Test Book', errcode: 0 }),
93
- } as any;
94
- const result = await fetchWithPage(mockPage, '/book/info');
95
- expect(result.title).toBe('Test Book');
96
- });
97
-
98
- it('throws FETCH_ERROR on HTTP error', async () => {
99
- const mockPage = {
100
- evaluate: vi.fn().mockResolvedValue({ _httpError: '403' }),
101
- } as any;
102
- await expect(fetchWithPage(mockPage, '/book/info')).rejects.toThrow('HTTP 403');
103
- });
104
- });
@@ -3,16 +3,20 @@
3
3
  *
4
4
  * Two API domains:
5
5
  * - WEB_API (weread.qq.com/web/*): public, Node.js fetch
6
- * - API (i.weread.qq.com/*): private, browser page.evaluate with cookies
6
+ * - API (i.weread.qq.com/*): private, Node.js fetch with cookies from browser
7
7
  */
8
8
 
9
9
  import { CliError } from '../../errors.js';
10
- import type { IPage } from '../../types.js';
10
+ import type { BrowserCookie, IPage } from '../../types.js';
11
11
 
12
12
  const WEB_API = 'https://weread.qq.com/web';
13
13
  const API = 'https://i.weread.qq.com';
14
14
  const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
15
15
 
16
+ function buildCookieHeader(cookies: BrowserCookie[]): string {
17
+ return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
18
+ }
19
+
16
20
  /**
17
21
  * Fetch a public WeRead web endpoint (Node.js direct fetch).
18
22
  * Used by search and ranking commands (browser: false).
@@ -36,29 +40,50 @@ export async function fetchWebApi(path: string, params?: Record<string, string>)
36
40
  }
37
41
 
38
42
  /**
39
- * Fetch a private WeRead API endpoint via browser page.evaluate.
40
- * Automatically carries cookies for authenticated requests.
43
+ * Fetch a private WeRead API endpoint with cookies extracted from the browser.
44
+ * The HTTP request itself runs in Node.js to avoid page-context CORS failures.
41
45
  */
42
- export async function fetchWithPage(page: IPage, path: string, params?: Record<string, string>): Promise<any> {
46
+ export async function fetchPrivateApi(page: IPage, path: string, params?: Record<string, string>): Promise<any> {
43
47
  const url = new URL(`${API}${path}`);
44
48
  if (params) {
45
49
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
46
50
  }
47
51
  const urlStr = url.toString();
48
- const data = await page.evaluate(`
49
- async () => {
50
- const res = await fetch(${JSON.stringify(urlStr)}, { credentials: "include" });
51
- if (!res.ok) return { _httpError: String(res.status) };
52
- try { return await res.json(); }
53
- catch { return { _httpError: 'JSON parse error (status ' + res.status + ')' }; }
54
- }
55
- `);
56
- if (data?._httpError) {
57
- throw new CliError('FETCH_ERROR', `HTTP ${data._httpError} for ${path}`, 'WeRead API may be temporarily unavailable');
52
+
53
+ const cookies = await page.getCookies({ url: urlStr });
54
+ const cookieHeader = buildCookieHeader(cookies);
55
+
56
+ let resp: Response;
57
+ try {
58
+ resp = await fetch(urlStr, {
59
+ headers: {
60
+ 'User-Agent': UA,
61
+ 'Origin': 'https://weread.qq.com',
62
+ 'Referer': 'https://weread.qq.com/',
63
+ ...(cookieHeader ? { 'Cookie': cookieHeader } : {}),
64
+ },
65
+ });
66
+ } catch (error) {
67
+ throw new CliError(
68
+ 'FETCH_ERROR',
69
+ `Failed to fetch ${path}: ${error instanceof Error ? error.message : String(error)}`,
70
+ 'WeRead API may be temporarily unavailable',
71
+ );
72
+ }
73
+
74
+ let data: any;
75
+ try {
76
+ data = await resp.json();
77
+ } catch {
78
+ throw new CliError('PARSE_ERROR', `Invalid JSON response for ${path}`, 'WeRead may have returned an HTML error page');
58
79
  }
59
- if (data?.errcode === -2010) {
80
+
81
+ if (resp.status === 401 || data?.errcode === -2010) {
60
82
  throw new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first');
61
83
  }
84
+ if (!resp.ok) {
85
+ throw new CliError('FETCH_ERROR', `HTTP ${resp.status} for ${path}`, 'WeRead API may be temporarily unavailable');
86
+ }
62
87
  if (data?.errcode != null && data.errcode !== 0) {
63
88
  throw new CliError('API_ERROR', data.errmsg ?? `WeRead API error ${data.errcode}`);
64
89
  }