@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,220 @@
1
+ import { JSDOM } from 'jsdom';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
4
+
5
+ vi.mock('node:fs', async (importOriginal) => {
6
+ const actual = await importOriginal();
7
+ return {
8
+ ...actual,
9
+ statSync: vi.fn((input) => {
10
+ const value = String(input);
11
+ if (value.includes('missing')) return undefined;
12
+ return { isFile: () => !value.includes('directory') };
13
+ }),
14
+ };
15
+ });
16
+
17
+ vi.mock('node:path', async (importOriginal) => {
18
+ const actual = await importOriginal();
19
+ return {
20
+ ...actual,
21
+ resolve: vi.fn((input) => `/abs/${input}`),
22
+ extname: vi.fn((input) => {
23
+ const match = String(input).match(/\.[^.]+$/);
24
+ return match ? match[0] : '';
25
+ }),
26
+ };
27
+ });
28
+
29
+ import { __test__, publishCommand } from './publish.js';
30
+
31
+ function makePage({ evaluateResults = [], overrides = {} } = {}) {
32
+ const evaluate = vi.fn();
33
+ for (const result of evaluateResults) {
34
+ evaluate.mockResolvedValueOnce(result);
35
+ }
36
+ evaluate.mockResolvedValue({ ok: false, reason: 'unknown-state' });
37
+
38
+ return {
39
+ goto: vi.fn().mockResolvedValue(undefined),
40
+ wait: vi.fn().mockResolvedValue(undefined),
41
+ evaluate,
42
+ setFileInput: vi.fn().mockResolvedValue(undefined),
43
+ getCurrentUrl: vi.fn().mockResolvedValue('https://www.goofish.com/publish'),
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ async function runBrowserScript(html, script, { url = 'https://www.goofish.com/publish' } = {}) {
49
+ const dom = new JSDOM(html, { url, runScripts: 'outside-only' });
50
+ return dom.window.eval(script);
51
+ }
52
+
53
+ const validArgs = {
54
+ title: 'MacBook Pro',
55
+ description: '成色很好,功能正常',
56
+ price: '5999.99',
57
+ condition: '轻微使用',
58
+ category: '笔记本',
59
+ };
60
+
61
+ describe('xianyu/publish', () => {
62
+ it('builds the goofish publish URL', () => {
63
+ expect(__test__.buildPublishUrl()).toBe('https://www.goofish.com/publish');
64
+ });
65
+
66
+ it('validates publish arguments before navigation', async () => {
67
+ const page = makePage();
68
+
69
+ await expect(publishCommand.func(page, { ...validArgs, title: ' ' })).rejects.toBeInstanceOf(ArgumentError);
70
+ await expect(publishCommand.func(page, { ...validArgs, price: '0' })).rejects.toBeInstanceOf(ArgumentError);
71
+ await expect(publishCommand.func(page, { ...validArgs, price: '12.345' })).rejects.toBeInstanceOf(ArgumentError);
72
+ await expect(publishCommand.func(page, { ...validArgs, condition: '八成新' })).rejects.toBeInstanceOf(ArgumentError);
73
+ await expect(publishCommand.func(page, { ...validArgs, images: 'a.bmp' })).rejects.toBeInstanceOf(ArgumentError);
74
+ await expect(publishCommand.func(page, { ...validArgs, images: 'missing.png' })).rejects.toBeInstanceOf(ArgumentError);
75
+ await expect(publishCommand.func(page, { ...validArgs, images: '1.png,2.png,3.png,4.png,5.png,6.png,7.png,8.png,9.png,10.png' })).rejects.toBeInstanceOf(ArgumentError);
76
+ expect(page.goto).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it('publishes when every UI step has positive proof', async () => {
80
+ const page = makePage({
81
+ evaluateResults: [
82
+ { hasPublishForm: true },
83
+ { ok: true },
84
+ { ok: true, filled: ['title', 'description', 'price', 'condition'], missing: [] },
85
+ { ok: true },
86
+ { status: 'published', item_id: '123456789012', url: 'https://www.goofish.com/item?id=123456789012' },
87
+ ],
88
+ });
89
+
90
+ const rows = await publishCommand.func(page, validArgs);
91
+
92
+ expect(rows).toEqual([{
93
+ status: 'published',
94
+ item_id: '123456789012',
95
+ title: 'MacBook Pro',
96
+ price: '¥5999.99',
97
+ condition: '轻微使用',
98
+ url: 'https://www.goofish.com/item?id=123456789012',
99
+ message: '发布成功',
100
+ }]);
101
+ });
102
+
103
+ it('uses IPage getCurrentUrl instead of a non-existent page.url method', async () => {
104
+ const page = makePage({
105
+ evaluateResults: [
106
+ { hasPublishForm: true },
107
+ { ok: true },
108
+ { ok: true, filled: ['title', 'description', 'price', 'condition'], missing: [] },
109
+ { ok: true },
110
+ { status: 'published', item_id: '123456789012' },
111
+ ],
112
+ overrides: {
113
+ getCurrentUrl: vi.fn().mockResolvedValue('https://www.goofish.com/item?id=123456789012'),
114
+ },
115
+ });
116
+
117
+ expect(page.url).toBeUndefined();
118
+
119
+ const rows = await publishCommand.func(page, validArgs);
120
+
121
+ expect(page.getCurrentUrl).toHaveBeenCalled();
122
+ expect(rows[0].url).toBe('https://www.goofish.com/item?id=123456789012');
123
+ });
124
+
125
+ it('maps login walls to AuthRequiredError', async () => {
126
+ const page = makePage({
127
+ evaluateResults: [
128
+ { requiresAuth: true },
129
+ ],
130
+ });
131
+
132
+ await expect(publishCommand.func(page, validArgs)).rejects.toBeInstanceOf(AuthRequiredError);
133
+ });
134
+
135
+ it('fails fast when category selection or form filling is not proven', async () => {
136
+ await expect(publishCommand.func(makePage({
137
+ evaluateResults: [
138
+ { hasPublishForm: true },
139
+ { ok: false, reason: 'category-not-found' },
140
+ ],
141
+ }), validArgs)).rejects.toBeInstanceOf(CommandExecutionError);
142
+
143
+ await expect(publishCommand.func(makePage({
144
+ evaluateResults: [
145
+ { hasPublishForm: true },
146
+ { ok: true },
147
+ { ok: false, missing: ['price'] },
148
+ ],
149
+ }), validArgs)).rejects.toBeInstanceOf(CommandExecutionError);
150
+ });
151
+
152
+ it('uploads validated local images through the discovered file input', async () => {
153
+ const page = makePage({
154
+ evaluateResults: [
155
+ { hasPublishForm: true },
156
+ { ok: true },
157
+ { ok: true, missing: [] },
158
+ { ok: true, selector: '[id="upload"]' },
159
+ { ok: true },
160
+ { status: 'published', item_id: '123456789012', url: 'https://www.goofish.com/item?id=123456789012' },
161
+ ],
162
+ });
163
+
164
+ await publishCommand.func(page, { ...validArgs, images: 'a.png,b.webp' });
165
+
166
+ expect(page.setFileInput).toHaveBeenCalledWith(['/abs/a.png', '/abs/b.webp'], '[id="upload"]');
167
+ });
168
+
169
+ it('does not return a success row for failed or unconfirmed publish states', async () => {
170
+ await expect(publishCommand.func(makePage({
171
+ evaluateResults: [
172
+ { hasPublishForm: true },
173
+ { ok: true },
174
+ { ok: true, missing: [] },
175
+ { ok: true },
176
+ { status: 'failed', message: '内容违规' },
177
+ ],
178
+ }), validArgs)).rejects.toBeInstanceOf(CommandExecutionError);
179
+
180
+ await expect(publishCommand.func(makePage({
181
+ evaluateResults: [
182
+ { hasPublishForm: true },
183
+ { ok: true },
184
+ { ok: true, missing: [] },
185
+ { ok: true },
186
+ ],
187
+ }), validArgs)).rejects.toBeInstanceOf(CommandExecutionError);
188
+ });
189
+
190
+ it('browser category script is async and returns typed failure reasons', async () => {
191
+ const result = await runBrowserScript('<main><button>其他</button></main>', __test__.buildSelectCategoryEvaluate('笔记本'));
192
+
193
+ expect(result).toEqual({ ok: false, reason: 'category-trigger-not-found' });
194
+ });
195
+
196
+ it('browser fill script reports missing required fields', async () => {
197
+ const result = await runBrowserScript(`
198
+ <main>
199
+ <input placeholder="标题" />
200
+ <textarea id="desc"></textarea>
201
+ <button>轻微使用</button>
202
+ </main>
203
+ `, __test__.buildFillFormEvaluate(validArgs));
204
+
205
+ expect(result.ok).toBe(false);
206
+ expect(result.missing).toContain('price');
207
+ });
208
+
209
+ it('browser success detector distinguishes success from failure and unknown states', async () => {
210
+ await expect(runBrowserScript('<body>发布成功</body>', __test__.buildDetectSuccessEvaluate(), {
211
+ url: 'https://www.goofish.com/item?id=123456789012',
212
+ })).resolves.toMatchObject({ status: 'published', item_id: '123456789012' });
213
+
214
+ await expect(runBrowserScript('<body><div class="error">内容违规</div></body>', __test__.buildDetectSuccessEvaluate()))
215
+ .resolves.toMatchObject({ status: 'failed', message: '内容违规' });
216
+
217
+ await expect(runBrowserScript('<body>处理中</body>', __test__.buildDetectSuccessEvaluate()))
218
+ .resolves.toEqual({ ok: false, reason: 'unknown-state' });
219
+ });
220
+ });
@@ -1,19 +1,58 @@
1
+ // Xiaoe (小鹅通) catalog — list chapters + sections of a course / column
2
+ // page (`h5.xet.citv.cn`).
3
+ //
4
+ // Replaces the legacy `pipeline:[]` form. The in-browser extraction logic
5
+ // (Vue store walking + auto-scroll-to-load + Vue child traversal) is kept
6
+ // byte-for-byte — Xiaoe's pages are SPA-rendered and Vue's private API
7
+ // (`__vue__`, `$store`, `$children`, `chapter_box.__vue__`) is the only
8
+ // stable hook we have. JSDOM cannot reproduce the Vue runtime tree, so
9
+ // reorganising the IIFE without live verify would be silent-failure risk.
10
+ //
11
+ // What changes:
12
+ // - `func` form + `Strategy.COOKIE` + `browser:true`.
13
+ // - Typed errors: `ArgumentError` on missing url; `EmptyResultError`
14
+ // when the IIFE yields zero rows (almost always means the cookie
15
+ // expired or the URL is not a course page); `CommandExecutionError`
16
+ // when `page.evaluate` rejects.
17
+ // - Three pure helpers (`typeLabel`, `buildItemUrl`, `chapterUrlPath`)
18
+ // are module-level exports and are embedded into the in-page IIFE
19
+ // via `${fn.toString()}` so the live and the test path share one
20
+ // source of truth.
21
+
1
22
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- cli({
3
- site: 'xiaoe',
4
- name: 'catalog',
5
- access: 'read',
6
- description: '小鹅通课程目录(支持普通课程、专栏、大专栏)',
7
- domain: 'h5.xet.citv.cn',
8
- strategy: Strategy.COOKIE,
9
- args: [
10
- { name: 'url', required: true, positional: true, help: '课程页面 URL' },
11
- ],
12
- columns: ['ch', 'chapter', 'no', 'title', 'type', 'resource_id', 'url', 'status'],
13
- pipeline: [
14
- { navigate: '${{ args.url }}' },
15
- { wait: 8 },
16
- { evaluate: `(async () => {
23
+ import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
24
+ import { requireXiaoePageUrl } from './content.js';
25
+
26
+ // resource_type → human label. 1=图文 2=直播 3=音频 4=视频 6=专栏 8=大专栏.
27
+ // Returns the raw `String(t)` when the type is unknown (e.g. xiaoe rolls
28
+ // out a new resource type) — never silently swallows it.
29
+ export function typeLabel(t) {
30
+ const map = { 1: '图文', 2: '直播', 3: '音频', 4: '视频', 6: '专栏', 8: '大专栏' };
31
+ return map[Number(t)] || String(t || '');
32
+ }
33
+
34
+ // Resolve a relative `jump_url` / `h5_url` / `url` against the page's
35
+ // origin. Returns '' when the item has no URL field at all.
36
+ export function buildItemUrl(item, origin) {
37
+ const u = item.jump_url || item.h5_url || item.url || '';
38
+ if (!u) return '';
39
+ return u.startsWith('http') ? u : (origin + u);
40
+ }
41
+
42
+ // chapter_type → URL path for the section reader. Xiaoe routes chapter
43
+ // types to different player paths; returning `undefined` for unknown
44
+ // types lets the caller decide whether to emit `''` instead of guessing
45
+ // a bad URL.
46
+ export function chapterUrlPath(chType) {
47
+ const map = { 1: '/v1/course/text/', 2: '/v2/course/alive/', 3: '/v1/course/audio/', 4: '/v1/course/video/' };
48
+ return map[Number(chType)];
49
+ }
50
+
51
+ export function buildCatalogScript() {
52
+ return `(async () => {
53
+ ${typeLabel.toString()}
54
+ ${buildItemUrl.toString()}
55
+ ${chapterUrlPath.toString()}
17
56
  var el = document.querySelector('#app');
18
57
  var store = (el && el.__vue__) ? el.__vue__.$store : null;
19
58
  if (!store) return [];
@@ -22,13 +61,6 @@ cli({
22
61
  var origin = window.location.origin;
23
62
  var courseName = coreInfo.resource_name || '';
24
63
 
25
- function typeLabel(t) {
26
- return {1:'图文',2:'直播',3:'音频',4:'视频',6:'专栏',8:'大专栏'}[Number(t)] || String(t||'');
27
- }
28
- function buildUrl(item) {
29
- var u = item.jump_url || item.h5_url || item.url || '';
30
- return (u && !u.startsWith('http')) ? origin + u : u;
31
- }
32
64
  function clickTab(name) {
33
65
  var tabs = document.querySelectorAll('span, div');
34
66
  for (var i = 0; i < tabs.length; i++) {
@@ -61,7 +93,7 @@ cli({
61
93
  if(scrollers[si].scrollHeight > scrollers[si].clientHeight) scrollers[si].scrollTop = scrollers[si].scrollHeight;
62
94
  }
63
95
  await new Promise(function(r) { setTimeout(r, 800); });
64
-
96
+
65
97
  // 点击可能存在的下拉/加载更多
66
98
  var moreTabs = document.querySelectorAll('span, div, p');
67
99
  for (var bi = 0; bi < moreTabs.length; bi++) {
@@ -70,7 +102,7 @@ cli({
70
102
  try { moreTabs[bi].click(); } catch(e){}
71
103
  }
72
104
  }
73
-
105
+
74
106
  var maxScrollHeight = getMaxScrollHeight(getScrollTargets());
75
107
  if (sc > 3 && maxScrollHeight === prevMaxScrollHeight) break;
76
108
  prevMaxScrollHeight = maxScrollHeight;
@@ -92,11 +124,13 @@ cli({
92
124
  var item = arr[j];
93
125
  if (!item.resource_id || !/^[pvlai]_/.test(item.resource_id)) continue;
94
126
  listData.push({
95
- ch: 1, chapter: courseName, no: j + 1,
127
+ ch: 1,
128
+ chapter: courseName,
129
+ no: j + 1,
96
130
  title: item.resource_title || item.title || item.chapter_title || '',
97
131
  type: typeLabel(item.resource_type || item.chapter_type),
98
132
  resource_id: item.resource_id,
99
- url: buildUrl(item),
133
+ url: buildItemUrl(item, origin),
100
134
  status: item.finished_state === 1 ? '已完成' : (item.resource_count ? item.resource_count + '节' : ''),
101
135
  });
102
136
  }
@@ -134,9 +168,11 @@ cli({
134
168
  var child = children[ck];
135
169
  var resId = child.resource_id || child.chapter_id || '';
136
170
  var chType = child.chapter_type || child.resource_type || 0;
137
- var urlPath = {1:'/v1/course/text/',2:'/v2/course/alive/',3:'/v1/course/audio/',4:'/v1/course/video/'}[Number(chType)];
171
+ var urlPath = chapterUrlPath(chType);
138
172
  result.push({
139
- ch: cj + 1, chapter: chTitle, no: ck + 1,
173
+ ch: cj + 1,
174
+ chapter: chTitle,
175
+ no: ck + 1,
140
176
  title: child.chapter_title || child.resource_title || '',
141
177
  type: typeLabel(chType),
142
178
  resource_id: resId,
@@ -146,17 +182,46 @@ cli({
146
182
  }
147
183
  }
148
184
  return result;
149
- })()
150
- ` },
151
- { map: {
152
- ch: '${{ item.ch }}',
153
- chapter: '${{ item.chapter }}',
154
- no: '${{ item.no }}',
155
- title: '${{ item.title }}',
156
- type: '${{ item.type }}',
157
- resource_id: '${{ item.resource_id }}',
158
- url: '${{ item.url }}',
159
- status: '${{ item.status }}',
160
- } },
185
+ })()`;
186
+ }
187
+
188
+ async function getXiaoeCatalog(page, args) {
189
+ const url = requireXiaoePageUrl(args.url, 'catalog');
190
+ let rows;
191
+ try {
192
+ await page.goto(url, { waitUntil: 'load', settleMs: 8000 });
193
+ rows = await page.evaluate(buildCatalogScript());
194
+ } catch (error) {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ throw new CommandExecutionError(
197
+ `Failed to read xiaoe catalog: ${message}`,
198
+ 'page may not have rendered or auth may be required',
199
+ );
200
+ }
201
+ if (!Array.isArray(rows) || rows.length === 0) {
202
+ throw new EmptyResultError(
203
+ 'xiaoe/catalog',
204
+ 'No catalog rows extracted — the URL may not be a course page or the login session has expired',
205
+ );
206
+ }
207
+ return rows;
208
+ }
209
+
210
+ export const catalogCommand = cli({
211
+ site: 'xiaoe',
212
+ name: 'catalog',
213
+ access: 'read',
214
+ description: '小鹅通课程目录(支持普通课程、专栏、大专栏)',
215
+ domain: 'h5.xet.citv.cn',
216
+ strategy: Strategy.COOKIE,
217
+ browser: true,
218
+ args: [
219
+ { name: 'url', required: true, positional: true, help: '课程页面 URL' },
161
220
  ],
221
+ columns: ['ch', 'chapter', 'no', 'title', 'type', 'resource_id', 'url', 'status'],
222
+ func: getXiaoeCatalog,
162
223
  });
224
+
225
+ export const __test__ = {
226
+ buildCatalogScript,
227
+ };
@@ -1,40 +1,175 @@
1
+ // Xiaoe (小鹅通) content extractor — pulls rendered article text from a
2
+ // rich-text page (h5.xet.citv.cn).
3
+ //
4
+ // Replaces the legacy `pipeline:[]` form. Two real bugs in the legacy
5
+ // adapter, both silent:
6
+ // 1. The IIFE returned `{title, content, content_length, image_count,
7
+ // images}` but `columns` declared `[title, content_length,
8
+ // image_count]` — `content` (the text the adapter exists to
9
+ // extract!) and `images` were silently dropped before reaching the
10
+ // caller. The user got "this article has 1234 chars" with no way
11
+ // to read those chars.
12
+ // 2. `JSON.stringify(images.slice(0, 20))` silently truncated to the
13
+ // first 20 image URLs and never told the caller the slice
14
+ // happened.
15
+ //
16
+ // New behavior:
17
+ // - `func` form + `Strategy.COOKIE` + `browser:true` (the page is
18
+ // gated behind a logged-in xiaoe session).
19
+ // - Pure helpers `pickContentText` / `countXiaoeImages` are
20
+ // module-level exports; the in-page IIFE embeds them via
21
+ // `${fn.toString()}` while JSDOM tests call the same exports
22
+ // directly against a hand-crafted fixture (same pattern as
23
+ // dianping #1313 / hupu #1387).
24
+ // - `content` is now a real column (the bug fix). `image_count` is
25
+ // metadata that helps callers decide whether to re-render the
26
+ // page for a JSON-image dump in a follow-up adapter.
27
+ // - Empty-content extraction → `EmptyResultError` with a
28
+ // login-likely hint (xiaoe routinely renders an empty shell when
29
+ // the cookie has expired). No silent `return [{ content: '' }]`.
30
+
1
31
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- cli({
32
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
33
+
34
+ export const CONTENT_SELECTORS = [
35
+ '.rich-text-wrap',
36
+ '.content-wrap',
37
+ '.article-content',
38
+ '.text-content',
39
+ '.course-detail',
40
+ '.detail-content',
41
+ '[class*="richtext"]',
42
+ '[class*="rich-text"]',
43
+ '.ql-editor',
44
+ ];
45
+ export const CONTENT_MIN_LENGTH = 50;
46
+
47
+ // Pure: walk `selectors` in order, return the trimmed text of the first
48
+ // element whose `.innerText` (with `.textContent` fallback for JSDOM)
49
+ // has more than `minLength` characters. Falls back to `<main>`, then
50
+ // `#app`, then `<body>` — same chain the legacy IIFE used.
51
+ export function pickContentText(doc, selectors, minLength = CONTENT_MIN_LENGTH) {
52
+ for (const sel of selectors) {
53
+ const el = doc.querySelector(sel);
54
+ if (!el) continue;
55
+ const text = ((el.innerText || el.textContent) || '').trim();
56
+ if (text.length > minLength) return text;
57
+ }
58
+ const fallback = doc.querySelector('main')
59
+ || doc.querySelector('#app')
60
+ || doc.body;
61
+ if (!fallback) return '';
62
+ return ((fallback.innerText || fallback.textContent) || '').trim();
63
+ }
64
+
65
+ // Pure: count `<img>` elements whose src looks like a real xiaoe-hosted
66
+ // asset. `data:` URIs and non-xiaoe CDNs (avatars, ads) are excluded.
67
+ export function countXiaoeImages(doc) {
68
+ let count = 0;
69
+ const imgs = doc.querySelectorAll('img');
70
+ for (let i = 0; i < imgs.length; i += 1) {
71
+ const src = imgs[i].getAttribute('src') || imgs[i].src || '';
72
+ if (!src) continue;
73
+ if (src.startsWith('data:')) continue;
74
+ if (!src.includes('xiaoe')) continue;
75
+ count += 1;
76
+ }
77
+ return count;
78
+ }
79
+
80
+ export function requireXiaoePageUrl(value, commandName) {
81
+ const raw = typeof value === 'string' ? value.trim() : '';
82
+ if (!raw) {
83
+ throw new ArgumentError('url is required (positional)');
84
+ }
85
+ let parsed;
86
+ try {
87
+ parsed = new URL(raw);
88
+ } catch {
89
+ throw new ArgumentError(
90
+ `invalid xiaoe URL: ${raw}`,
91
+ `Example: opencli xiaoe ${commandName} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`,
92
+ );
93
+ }
94
+ if (parsed.protocol !== 'https:') {
95
+ throw new ArgumentError(
96
+ `xiaoe URL must use https (got ${parsed.protocol.replace(':', '')})`,
97
+ `Example: opencli xiaoe ${commandName} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`,
98
+ );
99
+ }
100
+ const host = parsed.hostname.toLowerCase();
101
+ if (host !== 'h5.xet.citv.cn' && !host.endsWith('.h5.xet.citv.cn')) {
102
+ throw new ArgumentError(
103
+ `url must be on h5.xet.citv.cn or a shop subdomain (got ${parsed.hostname})`,
104
+ `Example: opencli xiaoe ${commandName} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`,
105
+ );
106
+ }
107
+ return parsed.toString();
108
+ }
109
+
110
+ export function buildContentScript() {
111
+ return `
112
+ (() => {
113
+ ${pickContentText.toString()}
114
+ ${countXiaoeImages.toString()}
115
+ const selectors = ${JSON.stringify(CONTENT_SELECTORS)};
116
+ const title = document.title || '';
117
+ const content = pickContentText(document, selectors, ${JSON.stringify(CONTENT_MIN_LENGTH)});
118
+ const imageCount = countXiaoeImages(document);
119
+ return [{
120
+ title,
121
+ content,
122
+ content_length: content.length,
123
+ image_count: imageCount,
124
+ }];
125
+ })()
126
+ `;
127
+ }
128
+
129
+ async function getXiaoeContent(page, args) {
130
+ const url = requireXiaoePageUrl(args.url, 'content');
131
+ let rows;
132
+ try {
133
+ await page.goto(url, { waitUntil: 'load', settleMs: 6000 });
134
+ rows = await page.evaluate(buildContentScript());
135
+ } catch (error) {
136
+ const message = error instanceof Error ? error.message : String(error);
137
+ throw new CommandExecutionError(
138
+ `Failed to extract xiaoe content: ${message}`,
139
+ 'page may not have rendered or auth may be required',
140
+ );
141
+ }
142
+ if (!Array.isArray(rows) || rows.length === 0) {
143
+ throw new EmptyResultError(
144
+ 'xiaoe/content',
145
+ 'No rows returned from page evaluator (page structure may have changed)',
146
+ );
147
+ }
148
+ const row = rows[0];
149
+ if (!row || typeof row.content !== 'string' || row.content.length === 0) {
150
+ throw new EmptyResultError(
151
+ 'xiaoe/content',
152
+ 'No article content extracted — login session may have expired or the page renders an empty shell',
153
+ );
154
+ }
155
+ return rows;
156
+ }
157
+
158
+ export const contentCommand = cli({
3
159
  site: 'xiaoe',
4
160
  name: 'content',
5
161
  access: 'read',
6
162
  description: '提取小鹅通图文页面内容为文本',
7
163
  domain: 'h5.xet.citv.cn',
8
164
  strategy: Strategy.COOKIE,
165
+ browser: true,
9
166
  args: [
10
167
  { name: 'url', required: true, positional: true, help: '页面 URL' },
11
168
  ],
12
- columns: ['title', 'content_length', 'image_count'],
13
- pipeline: [
14
- { navigate: '${{ args.url }}' },
15
- { wait: 6 },
16
- { evaluate: `(() => {
17
- var selectors = ['.rich-text-wrap','.content-wrap','.article-content','.text-content',
18
- '.course-detail','.detail-content','[class*="richtext"]','[class*="rich-text"]','.ql-editor'];
19
- var content = '';
20
- for (var i = 0; i < selectors.length; i++) {
21
- var el = document.querySelector(selectors[i]);
22
- if (el && el.innerText.trim().length > 50) { content = el.innerText.trim(); break; }
23
- }
24
- if (!content) content = (document.querySelector('main') || document.querySelector('#app') || document.body).innerText.trim();
25
-
26
- var images = [];
27
- document.querySelectorAll('img').forEach(function(img) {
28
- if (img.src && !img.src.startsWith('data:') && img.src.includes('xiaoe')) images.push(img.src);
29
- });
30
- return [{
31
- title: document.title,
32
- content: content,
33
- content_length: content.length,
34
- image_count: images.length,
35
- images: JSON.stringify(images.slice(0, 20)),
36
- }];
37
- })()
38
- ` },
39
- ],
169
+ columns: ['title', 'content', 'content_length', 'image_count'],
170
+ func: getXiaoeContent,
40
171
  });
172
+
173
+ export const __test__ = {
174
+ buildContentScript,
175
+ };