@jackwener/opencli 1.7.12 → 1.7.13

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 (407) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12194 -6843
  4. package/clis/1point3acres/digest.js +35 -0
  5. package/clis/1point3acres/forum.js +51 -0
  6. package/clis/1point3acres/forums.js +44 -0
  7. package/clis/1point3acres/hot.js +35 -0
  8. package/clis/1point3acres/latest.js +35 -0
  9. package/clis/1point3acres/notifications.js +64 -0
  10. package/clis/1point3acres/search.js +71 -0
  11. package/clis/1point3acres/thread.js +117 -0
  12. package/clis/1point3acres/user.js +77 -0
  13. package/clis/1point3acres/utils.js +247 -0
  14. package/clis/_shared/desktop-commands.js +4 -0
  15. package/clis/aibase/news.js +110 -0
  16. package/clis/aibase/news.test.js +59 -0
  17. package/clis/amazon/discussion.test.js +1 -28
  18. package/clis/antigravity/watch.js +3 -2
  19. package/clis/arxiv/author.js +44 -0
  20. package/clis/baidu-scholar/search.js +0 -1
  21. package/clis/bbc/topic.js +57 -0
  22. package/clis/bbc/utils.js +79 -0
  23. package/clis/chaoxing/assignments.js +1 -1
  24. package/clis/chaoxing/exams.js +1 -1
  25. package/clis/chatgpt/ask.js +57 -0
  26. package/clis/chatgpt/commands.test.js +45 -0
  27. package/clis/chatgpt/detail.js +46 -0
  28. package/clis/chatgpt/history.js +39 -0
  29. package/clis/chatgpt/image.js +12 -11
  30. package/clis/chatgpt/image.test.js +23 -0
  31. package/clis/chatgpt/new.js +25 -0
  32. package/clis/chatgpt/read.js +43 -0
  33. package/clis/chatgpt/send.js +46 -0
  34. package/clis/chatgpt/status.js +29 -0
  35. package/clis/chatgpt/utils.js +294 -4
  36. package/clis/chatgpt/utils.test.js +13 -0
  37. package/clis/chatgpt-app/ask.js +6 -3
  38. package/clis/chatwise/ask.js +16 -43
  39. package/clis/chatwise/composer.test.js +186 -0
  40. package/clis/chatwise/send.js +2 -24
  41. package/clis/chatwise/utils.js +143 -0
  42. package/clis/claude/ask.js +1 -1
  43. package/clis/claude/detail.js +1 -0
  44. package/clis/claude/history.js +1 -0
  45. package/clis/claude/new.js +1 -0
  46. package/clis/claude/read.js +1 -0
  47. package/clis/claude/send.js +1 -0
  48. package/clis/claude/status.js +1 -0
  49. package/clis/codex/ask.js +15 -9
  50. package/clis/codex/history.js +16 -33
  51. package/clis/codex/projects.js +28 -0
  52. package/clis/codex/read.js +10 -4
  53. package/clis/codex/send.js +10 -3
  54. package/clis/codex/sidebar.js +356 -0
  55. package/clis/codex/sidebar.test.js +329 -0
  56. package/clis/coingecko/categories.js +75 -0
  57. package/clis/coingecko/coin.js +107 -0
  58. package/clis/coingecko/coingecko.test.js +109 -0
  59. package/clis/coingecko/derivatives.js +84 -0
  60. package/clis/coingecko/exchanges.js +74 -0
  61. package/clis/coingecko/global.js +71 -0
  62. package/clis/coingecko/top.js +64 -0
  63. package/clis/coingecko/trending.js +55 -0
  64. package/clis/coupang/add-to-cart.js +21 -13
  65. package/clis/coupang/coupang.test.js +159 -0
  66. package/clis/coupang/product.js +257 -0
  67. package/clis/coupang/search.js +38 -16
  68. package/clis/coupang/utils.js +55 -1
  69. package/clis/crates/crate.js +62 -0
  70. package/clis/crates/search.js +44 -0
  71. package/clis/crates/utils.js +72 -0
  72. package/clis/ctrip/ctrip.test.js +234 -0
  73. package/clis/ctrip/hotel-suggest.js +45 -0
  74. package/clis/ctrip/search.js +22 -68
  75. package/clis/ctrip/utils.js +175 -0
  76. package/clis/cursor/ask.js +6 -3
  77. package/clis/dblp/author.js +133 -0
  78. package/clis/dblp/venue.js +64 -0
  79. package/clis/deepseek/ask.js +12 -7
  80. package/clis/deepseek/ask.test.js +13 -13
  81. package/clis/deepseek/detail.js +38 -0
  82. package/clis/deepseek/detail.test.js +81 -0
  83. package/clis/deepseek/history.js +1 -0
  84. package/clis/deepseek/new.js +1 -0
  85. package/clis/deepseek/read.js +1 -0
  86. package/clis/deepseek/send.js +140 -0
  87. package/clis/deepseek/send.test.js +107 -0
  88. package/clis/deepseek/status.js +1 -0
  89. package/clis/deepseek/utils.js +66 -0
  90. package/clis/deepseek/utils.test.js +107 -1
  91. package/clis/defillama/defillama.test.js +99 -0
  92. package/clis/defillama/protocol.js +84 -0
  93. package/clis/defillama/protocols.js +55 -0
  94. package/clis/defillama/utils.js +99 -0
  95. package/clis/devto/latest.js +74 -0
  96. package/clis/dockerhub/image.js +52 -0
  97. package/clis/dockerhub/search.js +47 -0
  98. package/clis/dockerhub/utils.js +100 -0
  99. package/clis/doubao/ask.js +7 -3
  100. package/clis/doubao/detail.js +1 -0
  101. package/clis/doubao/history.js +1 -0
  102. package/clis/doubao/meeting-summary.js +1 -0
  103. package/clis/doubao/meeting-transcript.js +1 -0
  104. package/clis/doubao/new.js +1 -0
  105. package/clis/doubao/read.js +1 -0
  106. package/clis/doubao/send.js +1 -0
  107. package/clis/doubao/status.js +1 -0
  108. package/clis/douyin/draft.test.js +1 -30
  109. package/clis/endoflife/endoflife.test.js +51 -0
  110. package/clis/endoflife/product.js +55 -0
  111. package/clis/endoflife/utils.js +89 -0
  112. package/clis/facebook/__fixtures__/notifications-page.html +13 -0
  113. package/clis/facebook/notifications.js +326 -30
  114. package/clis/facebook/notifications.test.js +458 -0
  115. package/clis/flathub/app.js +71 -0
  116. package/clis/flathub/flathub.test.js +90 -0
  117. package/clis/flathub/search.js +80 -0
  118. package/clis/flathub/utils.js +114 -0
  119. package/clis/gemini/ask.js +7 -3
  120. package/clis/gemini/ask.test.js +2 -2
  121. package/clis/gemini/deep-research-result.js +6 -2
  122. package/clis/gemini/deep-research-result.test.js +15 -14
  123. package/clis/gemini/deep-research.js +8 -4
  124. package/clis/gemini/deep-research.test.js +15 -18
  125. package/clis/gemini/image.js +7 -2
  126. package/clis/gemini/new.js +1 -0
  127. package/clis/gemini/utils.js +0 -4
  128. package/clis/google-scholar/cite.js +0 -1
  129. package/clis/google-scholar/profile.js +0 -1
  130. package/clis/google-scholar/search.js +0 -1
  131. package/clis/goproxy/goproxy.test.js +103 -0
  132. package/clis/goproxy/module.js +47 -0
  133. package/clis/goproxy/utils.js +165 -0
  134. package/clis/goproxy/versions.js +59 -0
  135. package/clis/gov-law/recent.js +0 -1
  136. package/clis/gov-law/search.js +0 -1
  137. package/clis/gov-policy/__fixtures__/recent.html +16 -0
  138. package/clis/gov-policy/__fixtures__/search.html +41 -0
  139. package/clis/gov-policy/gov-policy.test.js +224 -0
  140. package/clis/gov-policy/recent.js +66 -24
  141. package/clis/gov-policy/search.js +65 -23
  142. package/clis/gov-policy/utils.js +54 -0
  143. package/clis/grok/ask.js +49 -265
  144. package/clis/grok/ask.test.js +21 -46
  145. package/clis/grok/detail.js +60 -0
  146. package/clis/grok/history.js +48 -0
  147. package/clis/grok/{image.ts → image.js} +56 -70
  148. package/clis/grok/image.test.ts +20 -0
  149. package/clis/grok/new.js +20 -0
  150. package/clis/grok/read.js +39 -0
  151. package/clis/grok/send.js +50 -0
  152. package/clis/grok/status.js +41 -0
  153. package/clis/grok/utils.js +326 -0
  154. package/clis/grok/utils.test.js +103 -0
  155. package/clis/hf/datasets.js +88 -0
  156. package/clis/hf/hf.test.js +16 -0
  157. package/clis/hf/models.js +91 -0
  158. package/clis/hf/paper.js +79 -0
  159. package/clis/hf/spaces.js +101 -0
  160. package/clis/hf/top.js +1 -0
  161. package/clis/homebrew/cask.js +39 -0
  162. package/clis/homebrew/formula.js +41 -0
  163. package/clis/homebrew/popular.js +54 -0
  164. package/clis/homebrew/utils.js +100 -0
  165. package/clis/hupu/__fixtures__/hot-home.html +64 -0
  166. package/clis/hupu/detail.js +0 -1
  167. package/clis/hupu/hot.js +156 -35
  168. package/clis/hupu/hot.test.js +224 -0
  169. package/clis/hupu/search.js +0 -1
  170. package/clis/instagram/note.js +1 -1
  171. package/clis/instagram/note.test.js +1 -29
  172. package/clis/instagram/post.js +1 -1
  173. package/clis/instagram/post.test.js +1 -1
  174. package/clis/instagram/reel.js +1 -1
  175. package/clis/instagram/story.js +1 -1
  176. package/clis/instagram/story.test.js +1 -34
  177. package/clis/jd/commands.test.js +1 -24
  178. package/clis/lichess/lichess.test.js +85 -0
  179. package/clis/lichess/top.js +46 -0
  180. package/clis/lichess/user.js +91 -0
  181. package/clis/lichess/utils.js +97 -0
  182. package/clis/linkedin/search.js +107 -10
  183. package/clis/linkedin/search.test.js +222 -0
  184. package/clis/linux-do/feed.js +2 -5
  185. package/clis/linux-do/feed.test.js +35 -0
  186. package/clis/lobsters/domain.js +92 -0
  187. package/clis/maven/artifact.js +49 -0
  188. package/clis/maven/search.js +51 -0
  189. package/clis/maven/utils.js +110 -0
  190. package/clis/mdn/search.js +97 -0
  191. package/clis/medium/tag.js +135 -0
  192. package/clis/npm/downloads.js +59 -0
  193. package/clis/npm/package.js +70 -0
  194. package/clis/npm/search.js +49 -0
  195. package/clis/npm/utils.js +76 -0
  196. package/clis/nuget/nuget.test.js +111 -0
  197. package/clis/nuget/package.js +101 -0
  198. package/clis/nuget/search.js +69 -0
  199. package/clis/nuget/utils.js +87 -0
  200. package/clis/nvd/cve.js +121 -0
  201. package/clis/oeis/oeis.test.js +88 -0
  202. package/clis/oeis/search.js +63 -0
  203. package/clis/oeis/sequence.js +71 -0
  204. package/clis/oeis/utils.js +88 -0
  205. package/clis/openalex/search.js +69 -0
  206. package/clis/openalex/utils.js +160 -0
  207. package/clis/openalex/work.js +65 -0
  208. package/clis/openfda/drug-label.js +74 -0
  209. package/clis/openfda/food-recall.js +65 -0
  210. package/clis/openfda/openfda.test.js +114 -0
  211. package/clis/openfda/utils.js +67 -0
  212. package/clis/osv/osv.test.js +97 -0
  213. package/clis/osv/query.js +72 -0
  214. package/clis/osv/utils.js +169 -0
  215. package/clis/osv/vulnerability.js +54 -0
  216. package/clis/packagist/package.js +49 -0
  217. package/clis/packagist/search.js +43 -0
  218. package/clis/packagist/utils.js +113 -0
  219. package/clis/paperreview/feedback.js +1 -1
  220. package/clis/paperreview/review.js +1 -1
  221. package/clis/paperreview/submit.js +1 -1
  222. package/clis/pixiv/download.test.js +1 -1
  223. package/clis/pixiv/illusts.test.js +1 -1
  224. package/clis/pixiv/search.test.js +1 -1
  225. package/clis/pubmed/article.js +50 -0
  226. package/clis/pubmed/author.js +64 -0
  227. package/clis/pubmed/citations.js +36 -0
  228. package/clis/pubmed/pubmed.test.js +276 -0
  229. package/clis/pubmed/related.js +45 -0
  230. package/clis/pubmed/search.js +75 -0
  231. package/clis/pubmed/utils.js +309 -0
  232. package/clis/pypi/downloads.js +66 -0
  233. package/clis/pypi/package.js +79 -0
  234. package/clis/pypi/utils.js +55 -0
  235. package/clis/quark/mv.js +1 -1
  236. package/clis/quark/save.js +1 -1
  237. package/clis/qwen/ask.js +85 -0
  238. package/clis/qwen/detail.js +62 -0
  239. package/clis/qwen/history.js +61 -0
  240. package/clis/qwen/image.js +179 -0
  241. package/clis/qwen/new.js +23 -0
  242. package/clis/qwen/read.js +41 -0
  243. package/clis/qwen/send.js +55 -0
  244. package/clis/qwen/status.js +37 -0
  245. package/clis/qwen/utils.js +409 -0
  246. package/clis/qwen/utils.test.js +45 -0
  247. package/clis/rest-countries/country.js +65 -0
  248. package/clis/rest-countries/region.js +64 -0
  249. package/clis/rest-countries/rest-countries.test.js +83 -0
  250. package/clis/rest-countries/utils.js +126 -0
  251. package/clis/reuters/article-detail.js +53 -0
  252. package/clis/reuters/reuters.test.js +299 -0
  253. package/clis/reuters/search.js +45 -34
  254. package/clis/reuters/utils.js +159 -0
  255. package/clis/rfc/rfc.js +52 -0
  256. package/clis/rfc/rfc.test.js +74 -0
  257. package/clis/rfc/utils.js +72 -0
  258. package/clis/rubygems/gem.js +42 -0
  259. package/clis/rubygems/search.js +47 -0
  260. package/clis/rubygems/utils.js +86 -0
  261. package/clis/stackoverflow/related.js +66 -0
  262. package/clis/stackoverflow/stackoverflow.test.js +58 -0
  263. package/clis/stackoverflow/tag.js +60 -0
  264. package/clis/stackoverflow/user.js +50 -0
  265. package/clis/stackoverflow/utils.js +118 -0
  266. package/clis/steam/app.js +67 -0
  267. package/clis/steam/search.js +58 -0
  268. package/clis/steam/steam.test.js +46 -0
  269. package/clis/steam/utils.js +107 -0
  270. package/clis/taobao/commands.test.js +1 -24
  271. package/clis/test-utils.js +61 -0
  272. package/clis/tieba/hot.js +0 -1
  273. package/clis/tiktok/comment.js +128 -41
  274. package/clis/tiktok/creator-videos.js +270 -0
  275. package/clis/tiktok/creator-videos.test.js +113 -0
  276. package/clis/tiktok/explore.js +137 -29
  277. package/clis/tiktok/follow.js +115 -33
  278. package/clis/tiktok/following.js +157 -36
  279. package/clis/tiktok/friends.js +139 -37
  280. package/clis/tiktok/live.js +137 -41
  281. package/clis/tiktok/notifications.js +141 -38
  282. package/clis/tiktok/refactor.test.js +389 -0
  283. package/clis/tiktok/unfollow.js +124 -38
  284. package/clis/tiktok/user.js +203 -29
  285. package/clis/tiktok/utils.js +505 -0
  286. package/clis/tiktok/write-refactor.test.js +370 -0
  287. package/clis/toutiao/articles.js +36 -62
  288. package/clis/toutiao/hot.js +63 -0
  289. package/clis/toutiao/toutiao.test.js +378 -0
  290. package/clis/toutiao/utils.js +161 -0
  291. package/clis/tvmaze/search.js +61 -0
  292. package/clis/tvmaze/show.js +60 -0
  293. package/clis/tvmaze/tvmaze.test.js +93 -0
  294. package/clis/tvmaze/utils.js +110 -0
  295. package/clis/twitter/accept.js +1 -1
  296. package/clis/twitter/followers.js +134 -69
  297. package/clis/twitter/reply-dm.js +1 -1
  298. package/clis/twitter/reply.test.js +1 -29
  299. package/clis/uisdc/news.js +105 -0
  300. package/clis/uisdc/news.test.js +66 -0
  301. package/clis/wanfang/search.js +0 -1
  302. package/clis/web/read.js +47 -17
  303. package/clis/web/read.test.js +101 -1
  304. package/clis/weixin/create-draft.js +1 -1
  305. package/clis/weixin/drafts.js +1 -1
  306. package/clis/weixin/drafts.test.js +5 -1
  307. package/clis/weixin/search.js +157 -0
  308. package/clis/weixin/search.test.js +227 -0
  309. package/clis/wikidata/entity.js +60 -0
  310. package/clis/wikidata/search.js +50 -0
  311. package/clis/wikidata/utils.js +117 -0
  312. package/clis/wikidata/wikidata.test.js +83 -0
  313. package/clis/wikipedia/page.js +95 -0
  314. package/clis/wttr/current.js +63 -0
  315. package/clis/wttr/forecast.js +71 -0
  316. package/clis/wttr/utils.js +50 -0
  317. package/clis/wttr/wttr.test.js +84 -0
  318. package/clis/xianyu/chat.js +16 -4
  319. package/clis/xianyu/chat.test.js +64 -0
  320. package/clis/xianyu/publish.js +485 -0
  321. package/clis/xianyu/publish.test.js +220 -0
  322. package/clis/xiaoe/catalog.js +105 -40
  323. package/clis/xiaoe/content.js +164 -29
  324. package/clis/xiaoe/courses.js +86 -29
  325. package/clis/xiaoe/xiaoe.test.js +486 -0
  326. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  327. package/clis/xiaohongshu/publish.js +16 -3
  328. package/clis/xiaohongshu/publish.test.js +46 -1
  329. package/clis/youtube/transcript.js +13 -19
  330. package/clis/youtube/transcript.test.js +17 -0
  331. package/clis/yuanbao/ask.js +17 -66
  332. package/clis/yuanbao/ask.test.js +5 -5
  333. package/clis/yuanbao/detail.js +65 -0
  334. package/clis/yuanbao/history.js +51 -0
  335. package/clis/yuanbao/new.js +1 -0
  336. package/clis/yuanbao/read.js +38 -0
  337. package/clis/yuanbao/send.js +57 -0
  338. package/clis/yuanbao/shared.js +297 -5
  339. package/clis/yuanbao/shared.test.js +80 -0
  340. package/clis/yuanbao/status.js +44 -0
  341. package/clis/zlibrary/commands.test.js +1 -11
  342. package/dist/src/browser/base-page.d.ts +9 -0
  343. package/dist/src/browser/base-page.js +44 -1
  344. package/dist/src/browser/base-page.test.js +66 -0
  345. package/dist/src/browser/cdp.d.ts +1 -0
  346. package/dist/src/browser/cdp.js +51 -9
  347. package/dist/src/browser/daemon-client.d.ts +4 -0
  348. package/dist/src/browser/errors.js +1 -1
  349. package/dist/src/browser/page.d.ts +1 -1
  350. package/dist/src/browser/page.js +3 -1
  351. package/dist/src/browser/page.test.js +29 -0
  352. package/dist/src/browser/target-errors.d.ts +2 -1
  353. package/dist/src/browser/target-errors.js +1 -0
  354. package/dist/src/browser/target-resolver.d.ts +25 -0
  355. package/dist/src/browser/target-resolver.js +43 -0
  356. package/dist/src/build-manifest.js +9 -4
  357. package/dist/src/build-manifest.test.js +2 -8
  358. package/dist/src/capabilityRouting.d.ts +16 -1
  359. package/dist/src/capabilityRouting.js +24 -1
  360. package/dist/src/capabilityRouting.test.js +19 -1
  361. package/dist/src/cli.js +76 -11
  362. package/dist/src/cli.test.js +150 -0
  363. package/dist/src/commanderAdapter.js +0 -5
  364. package/dist/src/commanderAdapter.test.js +0 -1
  365. package/dist/src/discovery.js +2 -5
  366. package/dist/src/errors.js +1 -1
  367. package/dist/src/execution.d.ts +1 -1
  368. package/dist/src/execution.js +111 -27
  369. package/dist/src/execution.test.js +326 -17
  370. package/dist/src/help.d.ts +23 -2
  371. package/dist/src/help.js +41 -19
  372. package/dist/src/help.test.d.ts +1 -0
  373. package/dist/src/help.test.js +54 -0
  374. package/dist/src/main.js +14 -1
  375. package/dist/src/manifest-types.d.ts +5 -3
  376. package/dist/src/pipeline/executor.js +1 -1
  377. package/dist/src/pipeline/executor.test.js +8 -0
  378. package/dist/src/pipeline/registry.d.ts +9 -0
  379. package/dist/src/pipeline/registry.js +13 -1
  380. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  381. package/dist/src/pipeline/steps/browser.js +10 -0
  382. package/dist/src/pipeline/steps/download.test.js +1 -0
  383. package/dist/src/registry-api.d.ts +1 -1
  384. package/dist/src/registry.d.ts +12 -11
  385. package/dist/src/registry.js +16 -6
  386. package/dist/src/registry.test.js +2 -2
  387. package/dist/src/runtime.d.ts +2 -1
  388. package/dist/src/runtime.js +1 -1
  389. package/dist/src/serialization.d.ts +2 -2
  390. package/dist/src/serialization.js +4 -6
  391. package/dist/src/serialization.test.js +17 -0
  392. package/dist/src/types.d.ts +17 -0
  393. package/dist/src/validate.js +15 -11
  394. package/dist/src/validate.test.d.ts +9 -0
  395. package/dist/src/validate.test.js +90 -0
  396. package/package.json +1 -1
  397. package/scripts/fetch-adapters.js +1 -1
  398. package/scripts/typed-error-lint-baseline.json +5 -77
  399. package/clis/ctrip/search.test.js +0 -64
  400. package/clis/gov-policy/commands.test.js +0 -27
  401. package/clis/linux-do/category.js +0 -37
  402. package/clis/linux-do/hot.js +0 -26
  403. package/clis/linux-do/latest.js +0 -19
  404. package/clis/pixiv/test-utils.js +0 -23
  405. package/clis/toutiao/articles.test.js +0 -30
  406. package/dist/src/analysis.d.ts +0 -40
  407. package/dist/src/analysis.js +0 -172
@@ -0,0 +1,66 @@
1
+ import { JSDOM } from 'jsdom';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
4
+ import { uisdcNewsCommand, __test__ } from './news.js';
5
+
6
+ function runBrowserScript(html, script, url = 'https://www.uisdc.com/news') {
7
+ const dom = new JSDOM(html, { url, runScripts: 'outside-only' });
8
+ return dom.window.eval(script);
9
+ }
10
+
11
+ function makePage(evaluateResult) {
12
+ return {
13
+ goto: vi.fn().mockResolvedValue(undefined),
14
+ evaluate: vi.fn().mockResolvedValue(evaluateResult),
15
+ };
16
+ }
17
+
18
+ describe('uisdc/news', () => {
19
+ it('registers stable URL in columns', () => {
20
+ expect(uisdcNewsCommand.access).toBe('read');
21
+ expect(uisdcNewsCommand.columns).toEqual(['rank', 'title', 'summary', 'url']);
22
+ });
23
+
24
+ it('validates limit before browser navigation', async () => {
25
+ const page = makePage({ ok: true, rows: [] });
26
+ await expect(uisdcNewsCommand.func(page, { limit: 0 })).rejects.toBeInstanceOf(ArgumentError);
27
+ await expect(uisdcNewsCommand.func(page, { limit: 51 })).rejects.toBeInstanceOf(ArgumentError);
28
+ expect(page.goto).not.toHaveBeenCalled();
29
+ });
30
+
31
+ it('extracts rows from the UISDC news DOM', async () => {
32
+ const html = `
33
+ <div class="news-list">
34
+ <div class="news-item">
35
+ <div class="item-content">
36
+ <div class="dubao-items">
37
+ <div class="dubao-item">
38
+ <a href="/article-1"><span class="dubao-title"> AI design news </span></a>
39
+ <div class="dubao-content"> summary text </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ `;
46
+ const payload = runBrowserScript(html, __test__.buildExtractUisdcNewsJs());
47
+ const page = makePage(payload);
48
+
49
+ const rows = await uisdcNewsCommand.func(page, { limit: 10 });
50
+
51
+ expect(page.goto).toHaveBeenCalledWith('https://www.uisdc.com/news', { waitUntil: 'load', settleMs: 3000 });
52
+ expect(rows).toEqual([{
53
+ rank: 1,
54
+ title: 'AI design news',
55
+ summary: 'summary text',
56
+ url: 'https://www.uisdc.com/article-1',
57
+ }]);
58
+ });
59
+
60
+ it('maps selector drift and empty rows to typed errors', async () => {
61
+ await expect(uisdcNewsCommand.func(makePage({ ok: false, reason: 'selector-missing' }), { limit: 1 }))
62
+ .rejects.toBeInstanceOf(CommandExecutionError);
63
+ await expect(uisdcNewsCommand.func(makePage({ ok: true, rows: [{ title: '', url: '' }] }), { limit: 1 }))
64
+ .rejects.toBeInstanceOf(EmptyResultError);
65
+ });
66
+ });
@@ -14,7 +14,6 @@ cli({
14
14
  { name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
15
15
  ],
16
16
  columns: ['rank', 'title', 'authors', 'source', 'year', 'type', 'cited', 'url'],
17
- navigateBefore: false,
18
17
  func: async (page, kwargs) => {
19
18
  const limit = clampInt(kwargs.limit, 10, 1, 20);
20
19
  const query = requireNonEmptyQuery(kwargs.query);
package/clis/web/read.js CHANGED
@@ -18,6 +18,7 @@ import { downloadArticle } from '@jackwener/opencli/download/article-download';
18
18
 
19
19
  const NETWORK_IDLE_QUIET_MS = 1000;
20
20
  const NETWORK_IDLE_POLL_MS = 500;
21
+ const MIN_NON_STRUCTURAL_IFRAME_TEXT_CHARS = 50;
21
22
 
22
23
  function sleep(ms) {
23
24
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -31,7 +32,7 @@ function boolish(value) {
31
32
 
32
33
  function normalizeFrameMode(value) {
33
34
  const mode = String(value || 'same-origin').toLowerCase();
34
- if (['same-origin', 'none'].includes(mode)) return mode;
35
+ if (['same-origin', 'all-same-origin', 'none'].includes(mode)) return mode;
35
36
  return 'same-origin';
36
37
  }
37
38
 
@@ -144,6 +145,7 @@ function buildRenderAwareExtractorJs(options) {
144
145
  return `
145
146
  (() => {
146
147
  const frameMode = ${JSON.stringify(options.frames)};
148
+ const minNonStructuralIframeTextChars = ${MIN_NON_STRUCTURAL_IFRAME_TEXT_CHARS};
147
149
  const result = {
148
150
  title: '',
149
151
  author: '',
@@ -188,6 +190,7 @@ function buildRenderAwareExtractorJs(options) {
188
190
  const collectEmptyContainers = (root, scope, baseUrl) => {
189
191
  const likely = 'table, tbody, ul[id], ol[id], div[id], section[id], [class*="grid"], [class*="data"], [class*="list"], [id*="grid"], [id*="data"], [id*="list"]';
190
192
  root.querySelectorAll?.(likely).forEach((el) => {
193
+ if (scope === 'main' && el.closest?.('[data-opencli-iframe-source]')) return;
191
194
  const id = el.getAttribute('id') || '';
192
195
  const cls = el.getAttribute('class') || '';
193
196
  const name = [id, cls].join(' ').toLowerCase();
@@ -202,6 +205,29 @@ function buildRenderAwareExtractorJs(options) {
202
205
  });
203
206
  });
204
207
  };
208
+ const hasDataContainerSignal = (root) => {
209
+ const likely = 'table, tbody, ul[id], ol[id], [id*="grid"], [id*="data"], [id*="list"], [id*="content"], [id*="result"], [class*="grid"], [class*="data"], [class*="list"], [class*="content"], [class*="result"]';
210
+ return !!root.querySelector?.(likely);
211
+ };
212
+ const shouldIncludeExternalFrame = (frameBody) => {
213
+ // Outside-content iframes are less trusted than placeholders inside
214
+ // contentEl. Long plain text is the fallback for simple same-origin
215
+ // frames that lack article/table/list structure.
216
+ if (textLen(frameBody) >= minNonStructuralIframeTextChars) return true;
217
+ if (frameBody.querySelector?.('article, main, [role="main"], table, tbody, ul li, ol li')) return true;
218
+ return hasDataContainerSignal(frameBody);
219
+ };
220
+ const buildFrameSection = (frameBody, desc, fallbackLabel) => {
221
+ absolutizeTree(frameBody, desc.src || window.location.href);
222
+ collectEmptyContainers(frameBody, 'iframe', desc.src);
223
+ const section = document.createElement('section');
224
+ section.setAttribute('data-opencli-iframe-source', desc.src);
225
+ const heading = document.createElement('h2');
226
+ heading.textContent = '来自 iframe: ' + (desc.src || fallbackLabel);
227
+ section.appendChild(heading);
228
+ Array.from(frameBody.childNodes).forEach(node => section.appendChild(node));
229
+ return section;
230
+ };
205
231
 
206
232
  const ogTitle = document.querySelector('meta[property="og:title"]');
207
233
  if (ogTitle) result.title = ogTitle.getAttribute('content')?.trim() || '';
@@ -252,28 +278,32 @@ function buildRenderAwareExtractorJs(options) {
252
278
 
253
279
  const originalFrames = Array.from(contentEl.querySelectorAll('iframe'));
254
280
  const clonedFrames = Array.from(clone.querySelectorAll('iframe'));
281
+ const clonedFrameByOriginal = new Map();
282
+ originalFrames.forEach((frame, index) => {
283
+ const cloned = clonedFrames[index];
284
+ if (cloned) clonedFrameByOriginal.set(frame, cloned);
285
+ });
255
286
  const allFrames = Array.from(document.querySelectorAll('iframe'));
256
- result.diagnostics.frames = allFrames.map(describeFrame);
287
+ const frameDescriptions = new Map();
288
+ allFrames.forEach((frame, index) => frameDescriptions.set(frame, describeFrame(frame, index)));
289
+ const getFrameDescription = (frame, fallbackIndex) => frameDescriptions.get(frame) || describeFrame(frame, fallbackIndex);
290
+ result.diagnostics.frames = allFrames.map(frame => frameDescriptions.get(frame));
257
291
 
258
- if (frameMode === 'same-origin') {
259
- originalFrames.forEach((frame, index) => {
260
- const cloned = clonedFrames[index];
261
- if (!cloned) return;
262
- const desc = describeFrame(frame, index);
292
+ if (frameMode === 'same-origin' || frameMode === 'all-same-origin') {
293
+ allFrames.forEach((frame, index) => {
294
+ const insideContent = contentEl.contains(frame);
295
+ const cloned = insideContent ? clonedFrameByOriginal.get(frame) : null;
296
+ if (insideContent && !cloned) return;
297
+ const desc = getFrameDescription(frame, index);
263
298
  if (!desc.sameOrigin || !desc.accessible) return;
264
299
  try {
265
300
  const doc = frame.contentDocument;
266
301
  if (!doc?.body) return;
267
302
  const frameBody = doc.body.cloneNode(true);
268
- absolutizeTree(frameBody, desc.src || window.location.href);
269
- collectEmptyContainers(frameBody, 'iframe', desc.src);
270
- const section = document.createElement('section');
271
- section.setAttribute('data-opencli-iframe-source', desc.src);
272
- const heading = document.createElement('h2');
273
- heading.textContent = '来自 iframe: ' + (desc.src || frame.getAttribute('src') || ('#' + index));
274
- section.appendChild(heading);
275
- Array.from(frameBody.childNodes).forEach(node => section.appendChild(node));
276
- cloned.replaceWith(section);
303
+ if (frameMode !== 'all-same-origin' && !insideContent && !shouldIncludeExternalFrame(frameBody)) return;
304
+ const section = buildFrameSection(frameBody, desc, frame.getAttribute('src') || ('#' + index));
305
+ if (insideContent) cloned.replaceWith(section);
306
+ else clone.appendChild(section);
277
307
  result.diagnostics.includedFrameCount += 1;
278
308
  } catch {}
279
309
  });
@@ -376,7 +406,7 @@ const command = cli({
376
406
  { name: 'wait', type: 'int', default: 3, help: 'Seconds to wait after page load' },
377
407
  { name: 'wait-for', valueRequired: true, help: 'CSS selector to wait for in the main document or same-origin iframes' },
378
408
  { name: 'wait-until', default: 'domstable', choices: ['domstable', 'networkidle'], help: 'Readiness policy after navigation: domstable or networkidle' },
379
- { name: 'frames', default: 'same-origin', choices: ['same-origin', 'none'], help: 'Iframe handling mode: same-origin or none' },
409
+ { name: 'frames', default: 'same-origin', choices: ['same-origin', 'all-same-origin', 'none'], help: 'Iframe handling mode: relevant same-origin, all-same-origin, or none' },
380
410
  { name: 'diagnose', type: 'boolean', default: false, help: 'Print render diagnostics (frames, empty containers, XHR/API-like requests) to stderr' },
381
411
  { name: 'stdout', type: 'boolean', default: false, help: 'Print markdown to stdout instead of saving to a file' },
382
412
  ],
@@ -165,6 +165,18 @@ describe('web/read stdout behavior', () => {
165
165
  expect(page.evaluate.mock.calls[0]?.[0]).toContain('const frameMode = "none"');
166
166
  });
167
167
 
168
+ it('passes --frames all-same-origin into the extractor', async () => {
169
+ await read.func(page, {
170
+ url: 'https://example.com/article',
171
+ output: '/tmp/out',
172
+ 'download-images': false,
173
+ frames: 'all-same-origin',
174
+ stdout: false,
175
+ });
176
+
177
+ expect(page.evaluate.mock.calls[0]?.[0]).toContain('const frameMode = "all-same-origin"');
178
+ });
179
+
168
180
  it('fails fast when --wait-until networkidle is requested but capture is unavailable', async () => {
169
181
  page.startNetworkCapture.mockResolvedValue(false);
170
182
 
@@ -217,7 +229,7 @@ describe('web/read render-aware helpers', () => {
217
229
  `, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
218
230
  const frame = dom.window.document.querySelector('iframe');
219
231
  frame.contentDocument.open();
220
- frame.contentDocument.write('<body><table id="gridHd"><tr><th>Name</th></tr></table><ul id="gridDatas"><li>Station A 42</li></ul></body>');
232
+ frame.contentDocument.write('<body><table id="gridHd"><tr><th>Name</th></tr></table><ul id="gridDatas"></ul><p>Station A 42</p></body>');
221
233
  frame.contentDocument.close();
222
234
 
223
235
  const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'same-origin' }));
@@ -226,6 +238,94 @@ describe('web/read render-aware helpers', () => {
226
238
  expect(result.contentHtml).toContain('data-opencli-iframe-source="https://example.com/frame.html"');
227
239
  expect(result.contentHtml).toContain('来自 iframe: https://example.com/frame.html');
228
240
  expect(result.contentHtml).toContain('Station A 42');
241
+ expect(result.diagnostics.emptyContainers).toEqual(expect.arrayContaining([
242
+ expect.objectContaining({ scope: 'iframe', id: 'gridDatas', url: 'https://example.com/frame.html' }),
243
+ ]));
244
+ expect(result.diagnostics.emptyContainers.every(item => item.scope === 'iframe')).toBe(true);
245
+ });
246
+
247
+ it('merges readable same-origin iframes outside the selected content element', () => {
248
+ const dom = new JSDOM(`
249
+ <main>
250
+ <h1>Main Article</h1>
251
+ <p>${'Main content '.repeat(30)}</p>
252
+ </main>
253
+ <aside>
254
+ <iframe id="outside" src="/outside.html"></iframe>
255
+ </aside>
256
+ `, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
257
+ const frame = dom.window.document.querySelector('iframe');
258
+ frame.contentDocument.open();
259
+ frame.contentDocument.write(`<body><h1>Outside Frame</h1><p>${'Frame data '.repeat(12)}</p></body>`);
260
+ frame.contentDocument.close();
261
+
262
+ const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'same-origin' }));
263
+
264
+ expect(result.diagnostics.includedFrameCount).toBe(1);
265
+ expect(result.contentHtml).toContain('data-opencli-iframe-source="https://example.com/outside.html"');
266
+ expect(result.contentHtml).toContain('Outside Frame');
267
+ expect(result.contentHtml).toContain('Frame data');
268
+ });
269
+
270
+ it('keeps short data-like iframes outside the selected content element', () => {
271
+ const dom = new JSDOM(`
272
+ <main>
273
+ <h1>Main Article</h1>
274
+ <p>${'Main content '.repeat(30)}</p>
275
+ </main>
276
+ <iframe id="data-frame" src="/data.html"></iframe>
277
+ `, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
278
+ const frame = dom.window.document.querySelector('iframe');
279
+ frame.contentDocument.open();
280
+ frame.contentDocument.write('<body><table id="gridHd"><tr><th>水位</th></tr><tr><td>42</td></tr></table><ul id="gridDatas"></ul></body>');
281
+ frame.contentDocument.close();
282
+
283
+ const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'same-origin' }));
284
+
285
+ expect(result.diagnostics.includedFrameCount).toBe(1);
286
+ expect(result.contentHtml).toContain('42');
287
+ expect(result.diagnostics.emptyContainers).toEqual(expect.arrayContaining([
288
+ expect.objectContaining({ scope: 'iframe', id: 'gridDatas', url: 'https://example.com/data.html' }),
289
+ ]));
290
+ expect(result.diagnostics.emptyContainers.every(item => item.scope === 'iframe')).toBe(true);
291
+ });
292
+
293
+ it('skips short non-structural iframes outside the selected content element', () => {
294
+ const dom = new JSDOM(`
295
+ <main>
296
+ <h1>Main Article</h1>
297
+ <p>${'Main content '.repeat(30)}</p>
298
+ </main>
299
+ <iframe id="tiny-frame" src="/tiny.html"></iframe>
300
+ `, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
301
+ const frame = dom.window.document.querySelector('iframe');
302
+ frame.contentDocument.open();
303
+ frame.contentDocument.write('<body><p>tiny note</p></body>');
304
+ frame.contentDocument.close();
305
+
306
+ const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'same-origin' }));
307
+
308
+ expect(result.diagnostics.includedFrameCount).toBe(0);
309
+ expect(result.contentHtml).not.toContain('tiny note');
310
+ });
311
+
312
+ it('includes short non-structural iframes in all-same-origin mode', () => {
313
+ const dom = new JSDOM(`
314
+ <main>
315
+ <h1>Main Article</h1>
316
+ <p>${'Main content '.repeat(30)}</p>
317
+ </main>
318
+ <iframe id="status-frame" src="/status.html"></iframe>
319
+ `, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
320
+ const frame = dom.window.document.querySelector('iframe');
321
+ frame.contentDocument.open();
322
+ frame.contentDocument.write('<body><div>Online: 42°C</div></body>');
323
+ frame.contentDocument.close();
324
+
325
+ const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'all-same-origin' }));
326
+
327
+ expect(result.diagnostics.includedFrameCount).toBe(1);
328
+ expect(result.contentHtml).toContain('Online: 42°C');
229
329
  });
230
330
 
231
331
  it('marks API-like network entries as interesting and ignores static assets', () => {
@@ -179,13 +179,13 @@ export const createDraftCommand = cli({
179
179
  strategy: Strategy.COOKIE,
180
180
  browser: true,
181
181
  navigateBefore: false,
182
- timeoutSeconds: 180,
183
182
  args: [
184
183
  { name: 'title', required: true, help: '文章标题 (最长64字)' },
185
184
  { name: 'content', required: true, positional: true, help: '文章正文' },
186
185
  { name: 'author', help: '作者名 (最长8字)' },
187
186
  { name: 'cover-image', help: '封面图片路径 (会先上传到正文再设为封面)' },
188
187
  { name: 'summary', help: '文章摘要' },
188
+ { name: 'timeout', type: 'int', required: false, default: 180, help: 'Max seconds for the overall command (default: 180)' },
189
189
  ],
190
190
  columns: ['status', 'detail'],
191
191
 
@@ -12,9 +12,9 @@ export const draftsCommand = cli({
12
12
  strategy: Strategy.COOKIE,
13
13
  browser: true,
14
14
  navigateBefore: false,
15
- timeoutSeconds: 60,
16
15
  args: [
17
16
  { name: 'limit', type: 'int', default: 10, help: '最多显示条数' },
17
+ { name: 'timeout', type: 'int', required: false, default: 60, help: 'Max seconds for the overall command (default: 60)' },
18
18
  ],
19
19
  columns: ['Index', 'Title', 'Time'],
20
20
 
@@ -3,6 +3,7 @@ import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
3
3
  import { getRegistry } from '@jackwener/opencli/registry';
4
4
  import './create-draft.js';
5
5
  import './drafts.js';
6
+ import './search.js';
6
7
 
7
8
  function createPageMock(overrides = {}) {
8
9
  return {
@@ -18,7 +19,10 @@ describe('weixin command registration', () => {
18
19
  const registry = getRegistry();
19
20
  const values = [...registry.values()];
20
21
  expect(values.find(c => c.site === 'weixin' && c.name === 'create-draft')).toBeDefined();
21
- expect(values.find(c => c.site === 'weixin' && c.name === 'drafts')).toBeDefined();
22
+ const draftsCommand = values.find(c => c.site === 'weixin' && c.name === 'drafts');
23
+ expect(draftsCommand).toBeDefined();
24
+ expect(draftsCommand.args.find((arg) => arg.name === 'timeout')).toMatchObject({ type: 'int', default: 60 });
25
+ expect(values.find(c => c.site === 'weixin' && c.name === 'search')).toBeDefined();
22
26
  });
23
27
  });
24
28
 
@@ -0,0 +1,157 @@
1
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+
4
+ const SOGOU_WEIXIN_DOMAIN = 'weixin.sogou.com';
5
+ const DEFAULT_PAGE = 1;
6
+ const DEFAULT_LIMIT = 10;
7
+ const MAX_LIMIT = 10;
8
+
9
+ function normalizePositiveInteger(value, name, defaultValue, maxValue) {
10
+ if (value === undefined || value === null)
11
+ return defaultValue;
12
+ const text = String(value).trim();
13
+ if (!/^\d+$/.test(text)) {
14
+ throw new ArgumentError(`weixin search --${name} must be a positive integer`, `Pass --${name} as a whole number${maxValue ? ` from 1 to ${maxValue}` : ' greater than 0'}.`);
15
+ }
16
+ const parsed = Number(text);
17
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || (maxValue && parsed > maxValue)) {
18
+ throw new ArgumentError(`weixin search --${name} is out of range`, `Pass --${name} as a whole number${maxValue ? ` from 1 to ${maxValue}` : ' greater than 0'}.`);
19
+ }
20
+ return parsed;
21
+ }
22
+
23
+ function normalizePage(page) {
24
+ return normalizePositiveInteger(page, 'page', DEFAULT_PAGE);
25
+ }
26
+
27
+ function normalizeLimit(limit) {
28
+ return normalizePositiveInteger(limit, 'limit', DEFAULT_LIMIT, MAX_LIMIT);
29
+ }
30
+
31
+ function buildSearchUrl(query, pageNo) {
32
+ const searchUrl = new URL('https://weixin.sogou.com/weixin');
33
+ searchUrl.searchParams.set('query', query);
34
+ searchUrl.searchParams.set('type', '2');
35
+ searchUrl.searchParams.set('page', String(pageNo));
36
+ searchUrl.searchParams.set('ie', 'utf8');
37
+ return searchUrl.toString();
38
+ }
39
+
40
+ function buildExtractSearchResultsEvaluate() {
41
+ return String.raw`(() => {
42
+ const clean = (value) => {
43
+ return (value || '')
44
+ .replace(/\s+/g, ' ')
45
+ .replace(/<!--red_beg-->|<!--red_end-->/g, '')
46
+ .replace(/document\.write\(timeConvert\('\d+'\)\)/g, '')
47
+ .trim();
48
+ };
49
+
50
+ const absolutize = (href) => {
51
+ if (!href) return '';
52
+ try {
53
+ return new URL(href, window.location.origin).toString();
54
+ } catch {
55
+ return href;
56
+ }
57
+ };
58
+
59
+ const bodyText = clean(document.body && document.body.innerText);
60
+ const blocked = /验证码|安全验证|异常访问|访问过于频繁|请输入验证码/.test(bodyText);
61
+ const empty = /没有找到相关的微信文章|未找到相关|暂无相关|没有找到/.test(bodyText)
62
+ || Boolean(document.querySelector('.no-result, .no_result, .s-noresult'));
63
+ const cards = Array.from(document.querySelectorAll('.news-list li'));
64
+ const extracted = cards.map((item) => {
65
+ const linkEl = item.querySelector('h3 a[href]');
66
+ const summaryEl = item.querySelector('p.txt-info');
67
+ const timeEl = item.querySelector('.s-p .s2');
68
+ return {
69
+ title: clean(linkEl && linkEl.textContent),
70
+ url: absolutize(linkEl && linkEl.getAttribute('href')),
71
+ summary: clean(summaryEl && summaryEl.textContent),
72
+ publish_time: clean(timeEl && timeEl.textContent),
73
+ };
74
+ });
75
+ const rows = extracted.filter((row) => row.title && row.url);
76
+
77
+ return {
78
+ blocked,
79
+ empty,
80
+ cardCount: cards.length,
81
+ invalidCount: extracted.length - rows.length,
82
+ rows,
83
+ };
84
+ })()`;
85
+ }
86
+
87
+ cli({
88
+ site: 'weixin',
89
+ name: 'search',
90
+ access: 'read',
91
+ description: '使用搜狗微信搜索公众号文章;如需导出正文 Markdown,请使用 weixin download 处理公众号文章链接',
92
+ domain: SOGOU_WEIXIN_DOMAIN,
93
+ strategy: Strategy.PUBLIC,
94
+ browser: true,
95
+ args: [
96
+ { name: 'query', positional: true, required: true, help: '搜索关键词;如需正文 Markdown,请使用 weixin download 处理公众号文章链接' },
97
+ { name: 'page', type: 'int', default: 1, help: '结果页码,从 1 开始' },
98
+ { name: 'limit', type: 'int', default: 10, help: '返回条数,最大 10' },
99
+ ],
100
+ columns: ['rank', 'page', 'title', 'url', 'summary', 'publish_time'],
101
+ func: async (page, kwargs) => {
102
+ const query = String(kwargs.query ?? '').trim();
103
+ if (!query) {
104
+ throw new ArgumentError('A search query is required.', 'Pass a non-empty keyword to search Weixin articles via Sogou.');
105
+ }
106
+
107
+ const pageNo = normalizePage(kwargs.page);
108
+ const limit = normalizeLimit(kwargs.limit);
109
+ const searchUrl = buildSearchUrl(query, pageNo);
110
+
111
+ let payload;
112
+ try {
113
+ await page.goto(searchUrl);
114
+ await page.wait(2);
115
+ payload = await page.evaluate(buildExtractSearchResultsEvaluate());
116
+ }
117
+ catch (error) {
118
+ const detail = error instanceof Error ? error.message : String(error);
119
+ throw new CommandExecutionError('weixin search failed while loading Sogou results', detail);
120
+ }
121
+
122
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.rows)) {
123
+ throw new CommandExecutionError('weixin search returned an unreadable browser payload', 'Sogou Weixin may have changed its result page structure.');
124
+ }
125
+ if (payload.blocked) {
126
+ throw new CommandExecutionError('Sogou Weixin blocked this search request', 'Open weixin.sogou.com in Chrome and complete any verification before retrying.');
127
+ }
128
+ if (payload.invalidCount > 0) {
129
+ throw new CommandExecutionError('Sogou Weixin returned article cards without required title or URL', 'The result page structure may have changed; refusing to return a partial result set.');
130
+ }
131
+
132
+ const rows = payload.rows;
133
+ if (rows.length === 0 && payload.empty) {
134
+ throw new EmptyResultError('weixin search', 'Try a different keyword or a different page number.');
135
+ }
136
+ if (rows.length === 0) {
137
+ throw new CommandExecutionError('weixin search did not expose article result cards', 'Sogou Weixin may have changed its selectors or returned a transient shell page.');
138
+ }
139
+
140
+ return rows.slice(0, limit).map((row, index) => ({
141
+ rank: (pageNo - 1) * 10 + index + 1,
142
+ page: pageNo,
143
+ title: row.title,
144
+ url: row.url,
145
+ summary: row.summary,
146
+ publish_time: row.publish_time,
147
+ }));
148
+ },
149
+ });
150
+
151
+ export const __test__ = {
152
+ MAX_LIMIT,
153
+ normalizePage,
154
+ normalizeLimit,
155
+ buildSearchUrl,
156
+ buildExtractSearchResultsEvaluate,
157
+ };