@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.
- package/README.md +8 -7
- package/README.zh-CN.md +9 -8
- package/cli-manifest.json +12194 -6843
- package/clis/1point3acres/digest.js +35 -0
- package/clis/1point3acres/forum.js +51 -0
- package/clis/1point3acres/forums.js +44 -0
- package/clis/1point3acres/hot.js +35 -0
- package/clis/1point3acres/latest.js +35 -0
- package/clis/1point3acres/notifications.js +64 -0
- package/clis/1point3acres/search.js +71 -0
- package/clis/1point3acres/thread.js +117 -0
- package/clis/1point3acres/user.js +77 -0
- package/clis/1point3acres/utils.js +247 -0
- package/clis/_shared/desktop-commands.js +4 -0
- package/clis/aibase/news.js +110 -0
- package/clis/aibase/news.test.js +59 -0
- package/clis/amazon/discussion.test.js +1 -28
- package/clis/antigravity/watch.js +3 -2
- package/clis/arxiv/author.js +44 -0
- package/clis/baidu-scholar/search.js +0 -1
- package/clis/bbc/topic.js +57 -0
- package/clis/bbc/utils.js +79 -0
- package/clis/chaoxing/assignments.js +1 -1
- package/clis/chaoxing/exams.js +1 -1
- package/clis/chatgpt/ask.js +57 -0
- package/clis/chatgpt/commands.test.js +45 -0
- package/clis/chatgpt/detail.js +46 -0
- package/clis/chatgpt/history.js +39 -0
- package/clis/chatgpt/image.js +12 -11
- package/clis/chatgpt/image.test.js +23 -0
- package/clis/chatgpt/new.js +25 -0
- package/clis/chatgpt/read.js +43 -0
- package/clis/chatgpt/send.js +46 -0
- package/clis/chatgpt/status.js +29 -0
- package/clis/chatgpt/utils.js +294 -4
- package/clis/chatgpt/utils.test.js +13 -0
- package/clis/chatgpt-app/ask.js +6 -3
- package/clis/chatwise/ask.js +16 -43
- package/clis/chatwise/composer.test.js +186 -0
- package/clis/chatwise/send.js +2 -24
- package/clis/chatwise/utils.js +143 -0
- package/clis/claude/ask.js +1 -1
- package/clis/claude/detail.js +1 -0
- package/clis/claude/history.js +1 -0
- package/clis/claude/new.js +1 -0
- package/clis/claude/read.js +1 -0
- package/clis/claude/send.js +1 -0
- package/clis/claude/status.js +1 -0
- package/clis/codex/ask.js +15 -9
- package/clis/codex/history.js +16 -33
- package/clis/codex/projects.js +28 -0
- package/clis/codex/read.js +10 -4
- package/clis/codex/send.js +10 -3
- package/clis/codex/sidebar.js +356 -0
- package/clis/codex/sidebar.test.js +329 -0
- package/clis/coingecko/categories.js +75 -0
- package/clis/coingecko/coin.js +107 -0
- package/clis/coingecko/coingecko.test.js +109 -0
- package/clis/coingecko/derivatives.js +84 -0
- package/clis/coingecko/exchanges.js +74 -0
- package/clis/coingecko/global.js +71 -0
- package/clis/coingecko/top.js +64 -0
- package/clis/coingecko/trending.js +55 -0
- package/clis/coupang/add-to-cart.js +21 -13
- package/clis/coupang/coupang.test.js +159 -0
- package/clis/coupang/product.js +257 -0
- package/clis/coupang/search.js +38 -16
- package/clis/coupang/utils.js +55 -1
- package/clis/crates/crate.js +62 -0
- package/clis/crates/search.js +44 -0
- package/clis/crates/utils.js +72 -0
- package/clis/ctrip/ctrip.test.js +234 -0
- package/clis/ctrip/hotel-suggest.js +45 -0
- package/clis/ctrip/search.js +22 -68
- package/clis/ctrip/utils.js +175 -0
- package/clis/cursor/ask.js +6 -3
- package/clis/dblp/author.js +133 -0
- package/clis/dblp/venue.js +64 -0
- package/clis/deepseek/ask.js +12 -7
- package/clis/deepseek/ask.test.js +13 -13
- package/clis/deepseek/detail.js +38 -0
- package/clis/deepseek/detail.test.js +81 -0
- package/clis/deepseek/history.js +1 -0
- package/clis/deepseek/new.js +1 -0
- package/clis/deepseek/read.js +1 -0
- package/clis/deepseek/send.js +140 -0
- package/clis/deepseek/send.test.js +107 -0
- package/clis/deepseek/status.js +1 -0
- package/clis/deepseek/utils.js +66 -0
- package/clis/deepseek/utils.test.js +107 -1
- package/clis/defillama/defillama.test.js +99 -0
- package/clis/defillama/protocol.js +84 -0
- package/clis/defillama/protocols.js +55 -0
- package/clis/defillama/utils.js +99 -0
- package/clis/devto/latest.js +74 -0
- package/clis/dockerhub/image.js +52 -0
- package/clis/dockerhub/search.js +47 -0
- package/clis/dockerhub/utils.js +100 -0
- package/clis/doubao/ask.js +7 -3
- package/clis/doubao/detail.js +1 -0
- package/clis/doubao/history.js +1 -0
- package/clis/doubao/meeting-summary.js +1 -0
- package/clis/doubao/meeting-transcript.js +1 -0
- package/clis/doubao/new.js +1 -0
- package/clis/doubao/read.js +1 -0
- package/clis/doubao/send.js +1 -0
- package/clis/doubao/status.js +1 -0
- package/clis/douyin/draft.test.js +1 -30
- package/clis/endoflife/endoflife.test.js +51 -0
- package/clis/endoflife/product.js +55 -0
- package/clis/endoflife/utils.js +89 -0
- package/clis/facebook/__fixtures__/notifications-page.html +13 -0
- package/clis/facebook/notifications.js +326 -30
- package/clis/facebook/notifications.test.js +458 -0
- package/clis/flathub/app.js +71 -0
- package/clis/flathub/flathub.test.js +90 -0
- package/clis/flathub/search.js +80 -0
- package/clis/flathub/utils.js +114 -0
- package/clis/gemini/ask.js +7 -3
- package/clis/gemini/ask.test.js +2 -2
- package/clis/gemini/deep-research-result.js +6 -2
- package/clis/gemini/deep-research-result.test.js +15 -14
- package/clis/gemini/deep-research.js +8 -4
- package/clis/gemini/deep-research.test.js +15 -18
- package/clis/gemini/image.js +7 -2
- package/clis/gemini/new.js +1 -0
- package/clis/gemini/utils.js +0 -4
- package/clis/google-scholar/cite.js +0 -1
- package/clis/google-scholar/profile.js +0 -1
- package/clis/google-scholar/search.js +0 -1
- package/clis/goproxy/goproxy.test.js +103 -0
- package/clis/goproxy/module.js +47 -0
- package/clis/goproxy/utils.js +165 -0
- package/clis/goproxy/versions.js +59 -0
- package/clis/gov-law/recent.js +0 -1
- package/clis/gov-law/search.js +0 -1
- package/clis/gov-policy/__fixtures__/recent.html +16 -0
- package/clis/gov-policy/__fixtures__/search.html +41 -0
- package/clis/gov-policy/gov-policy.test.js +224 -0
- package/clis/gov-policy/recent.js +66 -24
- package/clis/gov-policy/search.js +65 -23
- package/clis/gov-policy/utils.js +54 -0
- package/clis/grok/ask.js +49 -265
- package/clis/grok/ask.test.js +21 -46
- package/clis/grok/detail.js +60 -0
- package/clis/grok/history.js +48 -0
- package/clis/grok/{image.ts → image.js} +56 -70
- package/clis/grok/image.test.ts +20 -0
- package/clis/grok/new.js +20 -0
- package/clis/grok/read.js +39 -0
- package/clis/grok/send.js +50 -0
- package/clis/grok/status.js +41 -0
- package/clis/grok/utils.js +326 -0
- package/clis/grok/utils.test.js +103 -0
- package/clis/hf/datasets.js +88 -0
- package/clis/hf/hf.test.js +16 -0
- package/clis/hf/models.js +91 -0
- package/clis/hf/paper.js +79 -0
- package/clis/hf/spaces.js +101 -0
- package/clis/hf/top.js +1 -0
- package/clis/homebrew/cask.js +39 -0
- package/clis/homebrew/formula.js +41 -0
- package/clis/homebrew/popular.js +54 -0
- package/clis/homebrew/utils.js +100 -0
- package/clis/hupu/__fixtures__/hot-home.html +64 -0
- package/clis/hupu/detail.js +0 -1
- package/clis/hupu/hot.js +156 -35
- package/clis/hupu/hot.test.js +224 -0
- package/clis/hupu/search.js +0 -1
- package/clis/instagram/note.js +1 -1
- package/clis/instagram/note.test.js +1 -29
- package/clis/instagram/post.js +1 -1
- package/clis/instagram/post.test.js +1 -1
- package/clis/instagram/reel.js +1 -1
- package/clis/instagram/story.js +1 -1
- package/clis/instagram/story.test.js +1 -34
- package/clis/jd/commands.test.js +1 -24
- package/clis/lichess/lichess.test.js +85 -0
- package/clis/lichess/top.js +46 -0
- package/clis/lichess/user.js +91 -0
- package/clis/lichess/utils.js +97 -0
- package/clis/linkedin/search.js +107 -10
- package/clis/linkedin/search.test.js +222 -0
- package/clis/linux-do/feed.js +2 -5
- package/clis/linux-do/feed.test.js +35 -0
- package/clis/lobsters/domain.js +92 -0
- package/clis/maven/artifact.js +49 -0
- package/clis/maven/search.js +51 -0
- package/clis/maven/utils.js +110 -0
- package/clis/mdn/search.js +97 -0
- package/clis/medium/tag.js +135 -0
- package/clis/npm/downloads.js +59 -0
- package/clis/npm/package.js +70 -0
- package/clis/npm/search.js +49 -0
- package/clis/npm/utils.js +76 -0
- package/clis/nuget/nuget.test.js +111 -0
- package/clis/nuget/package.js +101 -0
- package/clis/nuget/search.js +69 -0
- package/clis/nuget/utils.js +87 -0
- package/clis/nvd/cve.js +121 -0
- package/clis/oeis/oeis.test.js +88 -0
- package/clis/oeis/search.js +63 -0
- package/clis/oeis/sequence.js +71 -0
- package/clis/oeis/utils.js +88 -0
- package/clis/openalex/search.js +69 -0
- package/clis/openalex/utils.js +160 -0
- package/clis/openalex/work.js +65 -0
- package/clis/openfda/drug-label.js +74 -0
- package/clis/openfda/food-recall.js +65 -0
- package/clis/openfda/openfda.test.js +114 -0
- package/clis/openfda/utils.js +67 -0
- package/clis/osv/osv.test.js +97 -0
- package/clis/osv/query.js +72 -0
- package/clis/osv/utils.js +169 -0
- package/clis/osv/vulnerability.js +54 -0
- package/clis/packagist/package.js +49 -0
- package/clis/packagist/search.js +43 -0
- package/clis/packagist/utils.js +113 -0
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/pixiv/download.test.js +1 -1
- package/clis/pixiv/illusts.test.js +1 -1
- package/clis/pixiv/search.test.js +1 -1
- package/clis/pubmed/article.js +50 -0
- package/clis/pubmed/author.js +64 -0
- package/clis/pubmed/citations.js +36 -0
- package/clis/pubmed/pubmed.test.js +276 -0
- package/clis/pubmed/related.js +45 -0
- package/clis/pubmed/search.js +75 -0
- package/clis/pubmed/utils.js +309 -0
- package/clis/pypi/downloads.js +66 -0
- package/clis/pypi/package.js +79 -0
- package/clis/pypi/utils.js +55 -0
- package/clis/quark/mv.js +1 -1
- package/clis/quark/save.js +1 -1
- package/clis/qwen/ask.js +85 -0
- package/clis/qwen/detail.js +62 -0
- package/clis/qwen/history.js +61 -0
- package/clis/qwen/image.js +179 -0
- package/clis/qwen/new.js +23 -0
- package/clis/qwen/read.js +41 -0
- package/clis/qwen/send.js +55 -0
- package/clis/qwen/status.js +37 -0
- package/clis/qwen/utils.js +409 -0
- package/clis/qwen/utils.test.js +45 -0
- package/clis/rest-countries/country.js +65 -0
- package/clis/rest-countries/region.js +64 -0
- package/clis/rest-countries/rest-countries.test.js +83 -0
- package/clis/rest-countries/utils.js +126 -0
- package/clis/reuters/article-detail.js +53 -0
- package/clis/reuters/reuters.test.js +299 -0
- package/clis/reuters/search.js +45 -34
- package/clis/reuters/utils.js +159 -0
- package/clis/rfc/rfc.js +52 -0
- package/clis/rfc/rfc.test.js +74 -0
- package/clis/rfc/utils.js +72 -0
- package/clis/rubygems/gem.js +42 -0
- package/clis/rubygems/search.js +47 -0
- package/clis/rubygems/utils.js +86 -0
- package/clis/stackoverflow/related.js +66 -0
- package/clis/stackoverflow/stackoverflow.test.js +58 -0
- package/clis/stackoverflow/tag.js +60 -0
- package/clis/stackoverflow/user.js +50 -0
- package/clis/stackoverflow/utils.js +118 -0
- package/clis/steam/app.js +67 -0
- package/clis/steam/search.js +58 -0
- package/clis/steam/steam.test.js +46 -0
- package/clis/steam/utils.js +107 -0
- package/clis/taobao/commands.test.js +1 -24
- package/clis/test-utils.js +61 -0
- package/clis/tieba/hot.js +0 -1
- package/clis/tiktok/comment.js +128 -41
- package/clis/tiktok/creator-videos.js +270 -0
- package/clis/tiktok/creator-videos.test.js +113 -0
- package/clis/tiktok/explore.js +137 -29
- package/clis/tiktok/follow.js +115 -33
- package/clis/tiktok/following.js +157 -36
- package/clis/tiktok/friends.js +139 -37
- package/clis/tiktok/live.js +137 -41
- package/clis/tiktok/notifications.js +141 -38
- package/clis/tiktok/refactor.test.js +389 -0
- package/clis/tiktok/unfollow.js +124 -38
- package/clis/tiktok/user.js +203 -29
- package/clis/tiktok/utils.js +505 -0
- package/clis/tiktok/write-refactor.test.js +370 -0
- package/clis/toutiao/articles.js +36 -62
- package/clis/toutiao/hot.js +63 -0
- package/clis/toutiao/toutiao.test.js +378 -0
- package/clis/toutiao/utils.js +161 -0
- package/clis/tvmaze/search.js +61 -0
- package/clis/tvmaze/show.js +60 -0
- package/clis/tvmaze/tvmaze.test.js +93 -0
- package/clis/tvmaze/utils.js +110 -0
- package/clis/twitter/accept.js +1 -1
- package/clis/twitter/followers.js +134 -69
- package/clis/twitter/reply-dm.js +1 -1
- package/clis/twitter/reply.test.js +1 -29
- package/clis/uisdc/news.js +105 -0
- package/clis/uisdc/news.test.js +66 -0
- package/clis/wanfang/search.js +0 -1
- package/clis/web/read.js +47 -17
- package/clis/web/read.test.js +101 -1
- package/clis/weixin/create-draft.js +1 -1
- package/clis/weixin/drafts.js +1 -1
- package/clis/weixin/drafts.test.js +5 -1
- package/clis/weixin/search.js +157 -0
- package/clis/weixin/search.test.js +227 -0
- package/clis/wikidata/entity.js +60 -0
- package/clis/wikidata/search.js +50 -0
- package/clis/wikidata/utils.js +117 -0
- package/clis/wikidata/wikidata.test.js +83 -0
- package/clis/wikipedia/page.js +95 -0
- package/clis/wttr/current.js +63 -0
- package/clis/wttr/forecast.js +71 -0
- package/clis/wttr/utils.js +50 -0
- package/clis/wttr/wttr.test.js +84 -0
- package/clis/xianyu/chat.js +16 -4
- package/clis/xianyu/chat.test.js +64 -0
- package/clis/xianyu/publish.js +485 -0
- package/clis/xianyu/publish.test.js +220 -0
- package/clis/xiaoe/catalog.js +105 -40
- package/clis/xiaoe/content.js +164 -29
- package/clis/xiaoe/courses.js +86 -29
- package/clis/xiaoe/xiaoe.test.js +486 -0
- package/clis/xiaohongshu/creator-notes-summary.js +1 -1
- package/clis/xiaohongshu/publish.js +16 -3
- package/clis/xiaohongshu/publish.test.js +46 -1
- package/clis/youtube/transcript.js +13 -19
- package/clis/youtube/transcript.test.js +17 -0
- package/clis/yuanbao/ask.js +17 -66
- package/clis/yuanbao/ask.test.js +5 -5
- package/clis/yuanbao/detail.js +65 -0
- package/clis/yuanbao/history.js +51 -0
- package/clis/yuanbao/new.js +1 -0
- package/clis/yuanbao/read.js +38 -0
- package/clis/yuanbao/send.js +57 -0
- package/clis/yuanbao/shared.js +297 -5
- package/clis/yuanbao/shared.test.js +80 -0
- package/clis/yuanbao/status.js +44 -0
- package/clis/zlibrary/commands.test.js +1 -11
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +44 -1
- package/dist/src/browser/base-page.test.js +66 -0
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +51 -9
- package/dist/src/browser/daemon-client.d.ts +4 -0
- package/dist/src/browser/errors.js +1 -1
- package/dist/src/browser/page.d.ts +1 -1
- package/dist/src/browser/page.js +3 -1
- package/dist/src/browser/page.test.js +29 -0
- package/dist/src/browser/target-errors.d.ts +2 -1
- package/dist/src/browser/target-errors.js +1 -0
- package/dist/src/browser/target-resolver.d.ts +25 -0
- package/dist/src/browser/target-resolver.js +43 -0
- package/dist/src/build-manifest.js +9 -4
- package/dist/src/build-manifest.test.js +2 -8
- package/dist/src/capabilityRouting.d.ts +16 -1
- package/dist/src/capabilityRouting.js +24 -1
- package/dist/src/capabilityRouting.test.js +19 -1
- package/dist/src/cli.js +76 -11
- package/dist/src/cli.test.js +150 -0
- package/dist/src/commanderAdapter.js +0 -5
- package/dist/src/commanderAdapter.test.js +0 -1
- package/dist/src/discovery.js +2 -5
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +111 -27
- package/dist/src/execution.test.js +326 -17
- package/dist/src/help.d.ts +23 -2
- package/dist/src/help.js +41 -19
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +54 -0
- package/dist/src/main.js +14 -1
- package/dist/src/manifest-types.d.ts +5 -3
- package/dist/src/pipeline/executor.js +1 -1
- package/dist/src/pipeline/executor.test.js +8 -0
- package/dist/src/pipeline/registry.d.ts +9 -0
- package/dist/src/pipeline/registry.js +13 -1
- package/dist/src/pipeline/steps/browser.d.ts +1 -0
- package/dist/src/pipeline/steps/browser.js +10 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +12 -11
- package/dist/src/registry.js +16 -6
- package/dist/src/registry.test.js +2 -2
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/serialization.d.ts +2 -2
- package/dist/src/serialization.js +4 -6
- package/dist/src/serialization.test.js +17 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/validate.js +15 -11
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/package.json +1 -1
- package/scripts/fetch-adapters.js +1 -1
- package/scripts/typed-error-lint-baseline.json +5 -77
- package/clis/ctrip/search.test.js +0 -64
- package/clis/gov-policy/commands.test.js +0 -27
- package/clis/linux-do/category.js +0 -37
- package/clis/linux-do/hot.js +0 -26
- package/clis/linux-do/latest.js +0 -19
- package/clis/pixiv/test-utils.js +0 -23
- package/clis/toutiao/articles.test.js +0 -30
- package/dist/src/analysis.d.ts +0 -40
- package/dist/src/analysis.js +0 -172
package/clis/hupu/hot.js
CHANGED
|
@@ -1,42 +1,163 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Hupu hot/home threads — public SSR HTML scrape via in-page DOM walk.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the legacy `pipeline:[]` + `documentElement.outerHTML` regex,
|
|
4
|
+
// which had two anti-pattern problems:
|
|
5
|
+
// 1. Regex against `outerHTML` is brittle to whitespace / attribute order
|
|
6
|
+
// shifts and silently drops rows when the markup wobbles.
|
|
7
|
+
// 2. The regex captured every 9-digit thread anchor on the page, not just
|
|
8
|
+
// the rows inside `.t-info` containers — `.list-item` rows and pure
|
|
9
|
+
// navigation links got conflated.
|
|
10
|
+
//
|
|
11
|
+
// New behavior:
|
|
12
|
+
// - `func` form with `Strategy.PUBLIC` + `browser:true` (consistent with
|
|
13
|
+
// other public hupu adapters).
|
|
14
|
+
// - Upfront `--limit` validation: must be a positive integer ≤ 100, no
|
|
15
|
+
// silent clamp; `ArgumentError` on bad input.
|
|
16
|
+
// - DOM walk via `querySelectorAll('.t-info')` — same scope hupu's web
|
|
17
|
+
// client renders, so the row count matches what the human sees.
|
|
18
|
+
// - Enriched columns: `lights` (亮 count, int|null), `replies` (回复
|
|
19
|
+
// count, int|null), `forum` (sub-section name from sibling `.t-label`),
|
|
20
|
+
// `is_hot` (whether the page tagged the row with `class=" hot"` —
|
|
21
|
+
// transparent surfacing of hupu's own hot marker without filtering, so
|
|
22
|
+
// existing callers see the same row order).
|
|
23
|
+
// - Empty page → `EmptyResultError`, never silent `[]`.
|
|
24
|
+
// - Pure extraction (`extractHupuHotRowsFromDoc`) is a Node-side export
|
|
25
|
+
// so JSDOM-against-frozen-fixture tests can call it directly while the
|
|
26
|
+
// live IIFE embeds the same function via `${fn.toString()}` (mirrors
|
|
27
|
+
// dianping #1313 pattern).
|
|
28
|
+
|
|
29
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
30
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
31
|
+
|
|
32
|
+
export const HUPU_HOST = 'https://bbs.hupu.com';
|
|
33
|
+
export const HOT_LIMIT_DEFAULT = 20;
|
|
34
|
+
export const HOT_LIMIT_MAX = 100;
|
|
35
|
+
|
|
36
|
+
export function normalizeHotLimit(raw) {
|
|
37
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
38
|
+
return HOT_LIMIT_DEFAULT;
|
|
39
|
+
}
|
|
40
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
41
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > HOT_LIMIT_MAX) {
|
|
42
|
+
throw new ArgumentError(
|
|
43
|
+
`--limit must be a positive integer in [1, ${HOT_LIMIT_MAX}], got ${JSON.stringify(raw)}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Parse hupu count strings like "50亮", "359回复", "1.2万" → typed int.
|
|
50
|
+
// Returns null when the input does not look like a count we can read.
|
|
51
|
+
// `0` is preserved (real value), `null` means "we could not extract it"
|
|
52
|
+
// — never use `0` as an unknown sentinel here.
|
|
53
|
+
export function parseHupuCount(raw) {
|
|
54
|
+
if (raw === undefined || raw === null) return null;
|
|
55
|
+
const text = String(raw).trim();
|
|
56
|
+
if (!text) return null;
|
|
57
|
+
const match = text.match(/^([0-9]+(?:\.[0-9]+)?)\s*(万)?/);
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
const num = parseFloat(match[1]);
|
|
60
|
+
if (!Number.isFinite(num)) return null;
|
|
61
|
+
return Math.round(match[2] === '万' ? num * 10000 : num);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Pure extractor: walks `.t-info` containers and returns at most `limit`
|
|
65
|
+
// rows. `doc` is a Document (live `document` in browser, JSDOM document
|
|
66
|
+
// in tests). `parseCount` is injected so the same function works in both
|
|
67
|
+
// contexts (in-browser eval embeds it via `${fn.toString()}`, JSDOM tests
|
|
68
|
+
// pass the imported reference).
|
|
69
|
+
export function extractHupuHotRowsFromDoc(doc, limit, parseCount) {
|
|
70
|
+
const out = [];
|
|
71
|
+
const items = doc.querySelectorAll('.t-info');
|
|
72
|
+
for (let i = 0; i < items.length && out.length < limit; i += 1) {
|
|
73
|
+
const info = items[i];
|
|
74
|
+
const anchor = info.querySelector('a[href]');
|
|
75
|
+
if (!anchor) continue;
|
|
76
|
+
const href = anchor.getAttribute('href') || '';
|
|
77
|
+
const tidMatch = href.match(/^\/(\d{9})\.html$/);
|
|
78
|
+
if (!tidMatch) continue;
|
|
79
|
+
const tid = tidMatch[1];
|
|
80
|
+
const titleEl = anchor.querySelector('.t-title');
|
|
81
|
+
const title = titleEl ? (titleEl.textContent || '').trim() : '';
|
|
82
|
+
if (!title) continue;
|
|
83
|
+
const classes = (anchor.getAttribute('class') || '').split(/\s+/).filter(Boolean);
|
|
84
|
+
const isHot = classes.includes('hot');
|
|
85
|
+
const lightsEl = info.querySelector('.t-lights');
|
|
86
|
+
const repliesEl = info.querySelector('.t-replies');
|
|
87
|
+
const lights = parseCount(lightsEl ? lightsEl.textContent : null);
|
|
88
|
+
const replies = parseCount(repliesEl ? repliesEl.textContent : null);
|
|
89
|
+
// forum label sits beside `.t-info` inside `.list-item`
|
|
90
|
+
const listItem = info.parentElement;
|
|
91
|
+
const labelEl = listItem ? listItem.querySelector('.t-label a') : null;
|
|
92
|
+
const forum = labelEl ? (labelEl.textContent || '').trim() : '';
|
|
93
|
+
out.push({
|
|
94
|
+
rank: out.length + 1,
|
|
95
|
+
tid,
|
|
96
|
+
title,
|
|
97
|
+
lights,
|
|
98
|
+
replies,
|
|
99
|
+
forum,
|
|
100
|
+
is_hot: isHot,
|
|
101
|
+
url: `${HUPU_HOST}/${tid}.html`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildHotScript(limit) {
|
|
108
|
+
return `
|
|
109
|
+
(async () => {
|
|
110
|
+
const HUPU_HOST = ${JSON.stringify(HUPU_HOST)};
|
|
111
|
+
${parseHupuCount.toString()}
|
|
112
|
+
${extractHupuHotRowsFromDoc.toString()}
|
|
113
|
+
// Wait briefly for .t-info rows to render in case the page is still
|
|
114
|
+
// hydrating; bbs.hupu.com is mostly SSR so this returns fast.
|
|
115
|
+
const start = Date.now();
|
|
116
|
+
while (document.querySelectorAll('.t-info').length === 0 && Date.now() - start < 5000) {
|
|
117
|
+
await new Promise(r => setTimeout(r, 100));
|
|
118
|
+
}
|
|
119
|
+
return extractHupuHotRowsFromDoc(document, ${JSON.stringify(limit)}, parseHupuCount);
|
|
120
|
+
})()
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function getHupuHot(page, args) {
|
|
125
|
+
const limit = normalizeHotLimit(args.limit);
|
|
126
|
+
await page.goto(`${HUPU_HOST}/`, { waitUntil: 'load', settleMs: 1000 });
|
|
127
|
+
let rows;
|
|
128
|
+
try {
|
|
129
|
+
rows = await page.evaluate(buildHotScript(limit));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
throw new CommandExecutionError(
|
|
133
|
+
`Failed to read hupu hot threads: ${message}`,
|
|
134
|
+
'bbs.hupu.com may be unreachable or its markup may have changed',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
138
|
+
throw new EmptyResultError(
|
|
139
|
+
'hupu/hot',
|
|
140
|
+
'No threads found on bbs.hupu.com — page structure may have changed',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return rows;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const hotCommand = cli({
|
|
3
147
|
site: 'hupu',
|
|
4
148
|
name: 'hot',
|
|
5
149
|
access: 'read',
|
|
6
|
-
description: '
|
|
150
|
+
description: '虎扑首页热门帖子(含 lights / replies / forum / is_hot 列)',
|
|
7
151
|
domain: 'bbs.hupu.com',
|
|
152
|
+
strategy: Strategy.PUBLIC,
|
|
153
|
+
browser: true,
|
|
8
154
|
args: [
|
|
9
|
-
{ name: 'limit', type: 'int', default:
|
|
10
|
-
],
|
|
11
|
-
columns: ['rank', 'tid', 'title', 'url'],
|
|
12
|
-
pipeline: [
|
|
13
|
-
{ navigate: 'https://bbs.hupu.com/' },
|
|
14
|
-
{ evaluate: `(async () => {
|
|
15
|
-
// 从HTML中提取帖子信息(适配新的HTML结构)
|
|
16
|
-
const html = document.documentElement.outerHTML;
|
|
17
|
-
const posts = [];
|
|
18
|
-
|
|
19
|
-
// 匹配当前虎扑页面结构的正则表达式
|
|
20
|
-
// 结构: <a href="/638249612.html"...><span class="t-title">标题</span></a>
|
|
21
|
-
const regex = /<a[^>]*href="\\/(\\d{9})\\.html"[^>]*><span[^>]*class="t-title"[^>]*>([^<]+)<\\/span><\\/a>/g;
|
|
22
|
-
let match;
|
|
23
|
-
|
|
24
|
-
while ((match = regex.exec(html)) !== null && posts.length < \${{ args.limit }}) {
|
|
25
|
-
posts.push({
|
|
26
|
-
tid: match[1],
|
|
27
|
-
title: match[2].trim()
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return posts;
|
|
32
|
-
})()
|
|
33
|
-
` },
|
|
34
|
-
{ map: {
|
|
35
|
-
rank: '${{ index + 1 }}',
|
|
36
|
-
tid: '${{ item.tid }}',
|
|
37
|
-
title: '${{ item.title }}',
|
|
38
|
-
url: 'https://bbs.hupu.com/${{ item.tid }}.html',
|
|
39
|
-
} },
|
|
40
|
-
{ limit: '${{ args.limit }}' },
|
|
155
|
+
{ name: 'limit', type: 'int', default: HOT_LIMIT_DEFAULT, help: `Number of threads (1-${HOT_LIMIT_MAX})` },
|
|
41
156
|
],
|
|
157
|
+
columns: ['rank', 'tid', 'title', 'lights', 'replies', 'forum', 'is_hot', 'url'],
|
|
158
|
+
func: getHupuHot,
|
|
42
159
|
});
|
|
160
|
+
|
|
161
|
+
export const __test__ = {
|
|
162
|
+
buildHotScript,
|
|
163
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Hupu hot adapter — contract + JSDOM-against-frozen-fixture tests.
|
|
2
|
+
//
|
|
3
|
+
// Why a frozen-HTML fixture: the legacy adapter used a
|
|
4
|
+
// `documentElement.outerHTML` regex that would silently miss rows when
|
|
5
|
+
// hupu nudged whitespace or attribute order. Testing against a slim
|
|
6
|
+
// captured fixture in JSDOM proves the new querySelectorAll-based
|
|
7
|
+
// extractor handles the real markup shape — a mocked `page.evaluate()`
|
|
8
|
+
// alone cannot catch in-browser DOM bugs (lesson from dianping #1312
|
|
9
|
+
// → #1313).
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { JSDOM } from 'jsdom';
|
|
15
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
16
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
17
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
18
|
+
import {
|
|
19
|
+
HOT_LIMIT_DEFAULT,
|
|
20
|
+
HOT_LIMIT_MAX,
|
|
21
|
+
extractHupuHotRowsFromDoc,
|
|
22
|
+
hotCommand,
|
|
23
|
+
normalizeHotLimit,
|
|
24
|
+
parseHupuCount,
|
|
25
|
+
__test__,
|
|
26
|
+
} from './hot.js';
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const HOT_FIXTURE = readFileSync(join(__dirname, '__fixtures__/hot-home.html'), 'utf8');
|
|
30
|
+
|
|
31
|
+
describe('hupu/hot — registration', () => {
|
|
32
|
+
it('registers as PUBLIC + browser:true with the new column shape', () => {
|
|
33
|
+
const cmd = getRegistry().get('hupu/hot');
|
|
34
|
+
expect(cmd).toBe(hotCommand);
|
|
35
|
+
expect(cmd.browser).toBe(true);
|
|
36
|
+
expect(cmd.strategy).toBe('public');
|
|
37
|
+
expect(cmd.access).toBe('read');
|
|
38
|
+
expect(cmd.domain).toBe('bbs.hupu.com');
|
|
39
|
+
expect(cmd.columns).toEqual(['rank', 'tid', 'title', 'lights', 'replies', 'forum', 'is_hot', 'url']);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('hupu/hot — normalizeHotLimit', () => {
|
|
44
|
+
it('returns default when raw is missing / empty', () => {
|
|
45
|
+
expect(normalizeHotLimit(undefined)).toBe(HOT_LIMIT_DEFAULT);
|
|
46
|
+
expect(normalizeHotLimit(null)).toBe(HOT_LIMIT_DEFAULT);
|
|
47
|
+
expect(normalizeHotLimit('')).toBe(HOT_LIMIT_DEFAULT);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('accepts integer-coerceable values inside the cap', () => {
|
|
51
|
+
expect(normalizeHotLimit(1)).toBe(1);
|
|
52
|
+
expect(normalizeHotLimit('20')).toBe(20);
|
|
53
|
+
expect(normalizeHotLimit(HOT_LIMIT_MAX)).toBe(HOT_LIMIT_MAX);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('throws ArgumentError on out-of-range / non-integer / non-numeric — no silent clamp', () => {
|
|
57
|
+
expect(() => normalizeHotLimit(0)).toThrow(ArgumentError);
|
|
58
|
+
expect(() => normalizeHotLimit(-1)).toThrow(ArgumentError);
|
|
59
|
+
expect(() => normalizeHotLimit(HOT_LIMIT_MAX + 1)).toThrow(ArgumentError);
|
|
60
|
+
expect(() => normalizeHotLimit(1.5)).toThrow(ArgumentError);
|
|
61
|
+
expect(() => normalizeHotLimit('not-a-number')).toThrow(ArgumentError);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('hupu/hot — parseHupuCount', () => {
|
|
66
|
+
it('parses base counts', () => {
|
|
67
|
+
expect(parseHupuCount('50亮')).toBe(50);
|
|
68
|
+
expect(parseHupuCount('359回复')).toBe(359);
|
|
69
|
+
expect(parseHupuCount('0亮')).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('expands 万 multiplier', () => {
|
|
73
|
+
expect(parseHupuCount('1.2万亮')).toBe(12000);
|
|
74
|
+
expect(parseHupuCount('5.8万回复')).toBe(58000);
|
|
75
|
+
expect(parseHupuCount('1万')).toBe(10000);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns null for missing / unparseable input — never 0 as unknown sentinel', () => {
|
|
79
|
+
expect(parseHupuCount(null)).toBeNull();
|
|
80
|
+
expect(parseHupuCount(undefined)).toBeNull();
|
|
81
|
+
expect(parseHupuCount('')).toBeNull();
|
|
82
|
+
expect(parseHupuCount(' ')).toBeNull();
|
|
83
|
+
expect(parseHupuCount('abc')).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('hupu/hot — extractHupuHotRowsFromDoc against frozen fixture', () => {
|
|
88
|
+
function loadDocument() {
|
|
89
|
+
const dom = new JSDOM(HOT_FIXTURE, { url: 'https://bbs.hupu.com/' });
|
|
90
|
+
return dom.window.document;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
it('extracts all 6 fixture rows with full column shape (lights/replies/forum/is_hot)', () => {
|
|
94
|
+
const doc = loadDocument();
|
|
95
|
+
const rows = extractHupuHotRowsFromDoc(doc, 50, parseHupuCount);
|
|
96
|
+
|
|
97
|
+
expect(rows).toHaveLength(6);
|
|
98
|
+
expect(rows.some((row) => row.tid === '639999999')).toBe(false);
|
|
99
|
+
// Row 0: hot row, ASCII counts
|
|
100
|
+
expect(rows[0]).toEqual({
|
|
101
|
+
rank: 1,
|
|
102
|
+
tid: '639088523',
|
|
103
|
+
title: '网友曝三亚4只皮皮虾收费1035,官方凌晨通报',
|
|
104
|
+
lights: 50,
|
|
105
|
+
replies: 359,
|
|
106
|
+
forum: '步行街主干道',
|
|
107
|
+
is_hot: true,
|
|
108
|
+
url: 'https://bbs.hupu.com/639088523.html',
|
|
109
|
+
});
|
|
110
|
+
// Row 1: non-hot row — same forum, different is_hot
|
|
111
|
+
expect(rows[1]).toMatchObject({ rank: 2, tid: '639095236', is_hot: false });
|
|
112
|
+
// Row 2: 万 multiplier — covers 1.2万 → 12000, 5.8万 → 58000
|
|
113
|
+
expect(rows[2]).toMatchObject({ rank: 3, tid: '639094866', lights: 12000, replies: 58000, is_hot: false });
|
|
114
|
+
// Row 3: 0 is preserved as real 0, not coerced to null
|
|
115
|
+
expect(rows[3]).toMatchObject({ rank: 4, tid: '639100001', lights: 0, replies: 0, is_hot: true });
|
|
116
|
+
// Row 4: missing .t-lights span — null sentinel, never silent 0
|
|
117
|
+
expect(rows[4]).toMatchObject({ rank: 5, tid: '639100002', lights: null, replies: 20, is_hot: false });
|
|
118
|
+
// Row 5: forum varies — proves t-label resolution is per-row
|
|
119
|
+
expect(rows[5]).toMatchObject({ rank: 6, tid: '639100003', forum: '国际足球', is_hot: true });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('respects the limit cap (returns ≤ limit rows)', () => {
|
|
123
|
+
const doc = loadDocument();
|
|
124
|
+
expect(extractHupuHotRowsFromDoc(doc, 3, parseHupuCount)).toHaveLength(3);
|
|
125
|
+
expect(extractHupuHotRowsFromDoc(doc, 1, parseHupuCount)).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('returns [] when the document has no .t-info containers (page wobble guard)', () => {
|
|
129
|
+
const dom = new JSDOM('<html><body><div class="bbs-index-web"></div></body></html>', {
|
|
130
|
+
url: 'https://bbs.hupu.com/',
|
|
131
|
+
});
|
|
132
|
+
const rows = extractHupuHotRowsFromDoc(dom.window.document, 20, parseHupuCount);
|
|
133
|
+
expect(rows).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('skips rows whose href does not match /<9-digit>.html (silent partial guard)', () => {
|
|
137
|
+
const dom = new JSDOM(`
|
|
138
|
+
<html><body><div class="bbs-index-web">
|
|
139
|
+
<div class="list-item-wrap"><div class="list-wrap"><div class="list-item">
|
|
140
|
+
<div class="t-info">
|
|
141
|
+
<a href="/forum/announcement"><span class="t-title">公告</span></a>
|
|
142
|
+
<span class="t-lights">99亮</span><span class="t-replies">99回复</span>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="t-label"><a>导航</a></div>
|
|
145
|
+
</div></div></div>
|
|
146
|
+
<div class="list-item-wrap"><div class="list-wrap"><div class="list-item">
|
|
147
|
+
<div class="t-info">
|
|
148
|
+
<a href="/639200001.html" class=" hot"><span class="t-title">真帖子</span></a>
|
|
149
|
+
<span class="t-lights">5亮</span><span class="t-replies">3回复</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="t-label"><a>板块</a></div>
|
|
152
|
+
</div></div></div>
|
|
153
|
+
</div></body></html>`, { url: 'https://bbs.hupu.com/' });
|
|
154
|
+
const rows = extractHupuHotRowsFromDoc(dom.window.document, 20, parseHupuCount);
|
|
155
|
+
expect(rows).toHaveLength(1);
|
|
156
|
+
expect(rows[0]).toMatchObject({ tid: '639200001', is_hot: true });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('hupu/hot — buildHotScript invariants', () => {
|
|
161
|
+
it('embeds extractHupuHotRowsFromDoc + parseHupuCount via toString() and JSON.stringifies the limit', () => {
|
|
162
|
+
const script = __test__.buildHotScript(7);
|
|
163
|
+
expect(script).toContain('extractHupuHotRowsFromDoc');
|
|
164
|
+
expect(script).toContain('parseHupuCount');
|
|
165
|
+
expect(script).toContain('querySelectorAll');
|
|
166
|
+
// Limit must be embedded literally (JSON.stringified) so the IIFE
|
|
167
|
+
// can never be tricked into reading a parameter from the host page.
|
|
168
|
+
expect(script).toContain(', 7, parseHupuCount');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('does NOT use document.documentElement.outerHTML regex (anti-pattern guard)', () => {
|
|
172
|
+
const script = __test__.buildHotScript(20);
|
|
173
|
+
expect(script).not.toContain('documentElement.outerHTML');
|
|
174
|
+
expect(script).not.toMatch(/regex\.exec/);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('hupu/hot — getHupuHot func wiring', () => {
|
|
179
|
+
function createPageMock(rows) {
|
|
180
|
+
return {
|
|
181
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
182
|
+
evaluate: vi.fn().mockResolvedValue(rows),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createFailingPageMock(error) {
|
|
187
|
+
return {
|
|
188
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
189
|
+
evaluate: vi.fn().mockRejectedValue(error),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
it('validates --limit upfront BEFORE calling page.goto (no wasted navigation)', async () => {
|
|
194
|
+
const page = createPageMock([]);
|
|
195
|
+
await expect(hotCommand.func(page, { limit: 0 })).rejects.toThrow(ArgumentError);
|
|
196
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
197
|
+
expect(page.evaluate).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('throws EmptyResultError when the page returns an empty row list', async () => {
|
|
201
|
+
const page = createPageMock([]);
|
|
202
|
+
await expect(hotCommand.func(page, { limit: 20 })).rejects.toThrow(EmptyResultError);
|
|
203
|
+
expect(page.goto).toHaveBeenCalledTimes(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('wraps page.evaluate failures as CommandExecutionError', async () => {
|
|
207
|
+
const page = createFailingPageMock(new Error('selector crashed'));
|
|
208
|
+
await expect(hotCommand.func(page, { limit: 20 })).rejects.toThrow(CommandExecutionError);
|
|
209
|
+
expect(page.goto).toHaveBeenCalledTimes(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns the rows verbatim when the in-page extractor yields data', async () => {
|
|
213
|
+
const fakeRow = {
|
|
214
|
+
rank: 1, tid: '639000001', title: 'demo', lights: 1, replies: 2,
|
|
215
|
+
forum: '测试', is_hot: true, url: 'https://bbs.hupu.com/639000001.html',
|
|
216
|
+
};
|
|
217
|
+
const page = createPageMock([fakeRow]);
|
|
218
|
+
const result = await hotCommand.func(page, { limit: 20 });
|
|
219
|
+
expect(result).toEqual([fakeRow]);
|
|
220
|
+
expect(page.goto).toHaveBeenCalledWith('https://bbs.hupu.com/', expect.objectContaining({
|
|
221
|
+
waitUntil: 'load',
|
|
222
|
+
}));
|
|
223
|
+
});
|
|
224
|
+
});
|
package/clis/hupu/search.js
CHANGED
package/clis/instagram/note.js
CHANGED
|
@@ -203,9 +203,9 @@ cli({
|
|
|
203
203
|
domain: 'www.instagram.com',
|
|
204
204
|
strategy: Strategy.UI,
|
|
205
205
|
browser: true,
|
|
206
|
-
timeoutSeconds: 120,
|
|
207
206
|
args: [
|
|
208
207
|
{ name: 'content', positional: true, required: true, help: 'Note text (max 60 characters)' },
|
|
208
|
+
{ name: 'timeout', type: 'int', required: false, default: 120, help: 'Max seconds for the overall command (default: 120)' },
|
|
209
209
|
],
|
|
210
210
|
columns: ['status', 'detail', 'noteId'],
|
|
211
211
|
validateArgs: validateInstagramNoteArgs,
|
|
@@ -2,35 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
3
3
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
4
|
import './note.js';
|
|
5
|
-
|
|
6
|
-
return {
|
|
7
|
-
goto: vi.fn().mockResolvedValue(undefined),
|
|
8
|
-
evaluate: vi.fn().mockResolvedValue(undefined),
|
|
9
|
-
getCookies: vi.fn().mockResolvedValue([]),
|
|
10
|
-
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
11
|
-
click: vi.fn().mockResolvedValue(undefined),
|
|
12
|
-
typeText: vi.fn().mockResolvedValue(undefined),
|
|
13
|
-
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
14
|
-
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
15
|
-
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
16
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
17
|
-
tabs: vi.fn().mockResolvedValue([]),
|
|
18
|
-
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
19
|
-
newTab: vi.fn().mockResolvedValue(undefined),
|
|
20
|
-
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
21
|
-
networkRequests: vi.fn().mockResolvedValue([]),
|
|
22
|
-
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
23
|
-
scroll: vi.fn().mockResolvedValue(undefined),
|
|
24
|
-
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
25
|
-
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
26
|
-
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
27
|
-
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
28
|
-
screenshot: vi.fn().mockResolvedValue(''),
|
|
29
|
-
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
30
|
-
insertText: vi.fn().mockResolvedValue(undefined),
|
|
31
|
-
getCurrentUrl: vi.fn().mockResolvedValue(null),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
5
|
+
import { createPageMock } from '../test-utils.js';
|
|
34
6
|
describe('instagram note registration', () => {
|
|
35
7
|
beforeEach(() => {
|
|
36
8
|
vi.restoreAllMocks();
|
package/clis/instagram/post.js
CHANGED
|
@@ -1402,10 +1402,10 @@ cli({
|
|
|
1402
1402
|
domain: 'www.instagram.com',
|
|
1403
1403
|
strategy: Strategy.UI,
|
|
1404
1404
|
browser: true,
|
|
1405
|
-
timeoutSeconds: 300,
|
|
1406
1405
|
args: [
|
|
1407
1406
|
{ name: 'media', required: false, valueRequired: true, help: `Comma-separated media paths (images/videos, up to ${MAX_MEDIA_ITEMS})` },
|
|
1408
1407
|
{ name: 'content', positional: true, required: false, help: 'Caption text' },
|
|
1408
|
+
{ name: 'timeout', type: 'int', required: false, default: 300, help: 'Max seconds for the overall command (default: 300)' },
|
|
1409
1409
|
],
|
|
1410
1410
|
columns: ['status', 'detail', 'url'],
|
|
1411
1411
|
validateArgs: validateInstagramPostArgs,
|
|
@@ -370,7 +370,7 @@ describe('instagram post registration', () => {
|
|
|
370
370
|
const cmd = getRegistry().get('instagram/post');
|
|
371
371
|
expect(cmd).toBeDefined();
|
|
372
372
|
expect(cmd?.browser).toBe(true);
|
|
373
|
-
expect(cmd?.
|
|
373
|
+
expect(cmd?.args.some((arg) => arg.name === 'timeout' && arg.default === 300)).toBe(true);
|
|
374
374
|
expect(cmd?.args.some((arg) => arg.name === 'media' && !arg.required && arg.valueRequired)).toBe(true);
|
|
375
375
|
expect(cmd?.args.some((arg) => arg.name === 'content' && !arg.required && arg.positional)).toBe(true);
|
|
376
376
|
});
|
package/clis/instagram/reel.js
CHANGED
|
@@ -737,10 +737,10 @@ cli({
|
|
|
737
737
|
domain: 'www.instagram.com',
|
|
738
738
|
strategy: Strategy.UI,
|
|
739
739
|
browser: true,
|
|
740
|
-
timeoutSeconds: INSTAGRAM_REEL_TIMEOUT_SECONDS,
|
|
741
740
|
args: [
|
|
742
741
|
{ name: 'video', required: false, valueRequired: true, help: 'Path to a single .mp4 video file' },
|
|
743
742
|
{ name: 'content', positional: true, required: false, help: 'Caption text' },
|
|
743
|
+
{ name: 'timeout', type: 'int', required: false, default: INSTAGRAM_REEL_TIMEOUT_SECONDS, help: `Max seconds for the overall command (default: ${INSTAGRAM_REEL_TIMEOUT_SECONDS})` },
|
|
744
744
|
],
|
|
745
745
|
columns: ['status', 'detail', 'url'],
|
|
746
746
|
validateArgs: validateInstagramReelArgs,
|
package/clis/instagram/story.js
CHANGED
|
@@ -87,9 +87,9 @@ cli({
|
|
|
87
87
|
domain: 'www.instagram.com',
|
|
88
88
|
strategy: Strategy.UI,
|
|
89
89
|
browser: true,
|
|
90
|
-
timeoutSeconds: 300,
|
|
91
90
|
args: [
|
|
92
91
|
{ name: 'media', required: false, valueRequired: true, help: 'Path to a single story image or video file' },
|
|
92
|
+
{ name: 'timeout', type: 'int', required: false, default: 300, help: 'Max seconds for the overall command (default: 300)' },
|
|
93
93
|
],
|
|
94
94
|
columns: ['status', 'detail', 'url'],
|
|
95
95
|
validateArgs: validateInstagramStoryArgs,
|
|
@@ -6,6 +6,7 @@ import { ArgumentError } from '@jackwener/opencli/errors';
|
|
|
6
6
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
7
7
|
import * as privatePublish from './_shared/private-publish.js';
|
|
8
8
|
import './story.js';
|
|
9
|
+
import { createPageMock } from '../test-utils.js';
|
|
9
10
|
const tempDirs = [];
|
|
10
11
|
function createTempFile(name, bytes = Buffer.from('story-media')) {
|
|
11
12
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-instagram-story-'));
|
|
@@ -14,40 +15,6 @@ function createTempFile(name, bytes = Buffer.from('story-media')) {
|
|
|
14
15
|
fs.writeFileSync(filePath, bytes);
|
|
15
16
|
return filePath;
|
|
16
17
|
}
|
|
17
|
-
function createPageMock(evaluateResults = [], overrides = {}) {
|
|
18
|
-
const evaluate = vi.fn();
|
|
19
|
-
for (const result of evaluateResults) {
|
|
20
|
-
evaluate.mockResolvedValueOnce(result);
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
goto: vi.fn().mockResolvedValue(undefined),
|
|
24
|
-
evaluate,
|
|
25
|
-
getCookies: vi.fn().mockResolvedValue([]),
|
|
26
|
-
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
27
|
-
click: vi.fn().mockResolvedValue(undefined),
|
|
28
|
-
typeText: vi.fn().mockResolvedValue(undefined),
|
|
29
|
-
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
30
|
-
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
31
|
-
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
32
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
33
|
-
tabs: vi.fn().mockResolvedValue([]),
|
|
34
|
-
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
35
|
-
newTab: vi.fn().mockResolvedValue(undefined),
|
|
36
|
-
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
37
|
-
networkRequests: vi.fn().mockResolvedValue([]),
|
|
38
|
-
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
39
|
-
scroll: vi.fn().mockResolvedValue(undefined),
|
|
40
|
-
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
41
|
-
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
42
|
-
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
43
|
-
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
44
|
-
screenshot: vi.fn().mockResolvedValue(''),
|
|
45
|
-
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
46
|
-
insertText: vi.fn().mockResolvedValue(undefined),
|
|
47
|
-
getCurrentUrl: vi.fn().mockResolvedValue(null),
|
|
48
|
-
...overrides,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
18
|
afterAll(() => {
|
|
52
19
|
for (const dir of tempDirs) {
|
|
53
20
|
fs.rmSync(dir, { recursive: true, force: true });
|
package/clis/jd/commands.test.js
CHANGED
|
@@ -5,30 +5,7 @@ import './detail.js';
|
|
|
5
5
|
import './reviews.js';
|
|
6
6
|
import './cart.js';
|
|
7
7
|
import './add-cart.js';
|
|
8
|
-
|
|
9
|
-
return {
|
|
10
|
-
goto: vi.fn().mockResolvedValue(undefined),
|
|
11
|
-
evaluate: vi.fn().mockResolvedValue({ title: 'Demo', price: '¥99' }),
|
|
12
|
-
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
13
|
-
click: vi.fn().mockResolvedValue(undefined),
|
|
14
|
-
typeText: vi.fn().mockResolvedValue(undefined),
|
|
15
|
-
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
16
|
-
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
17
|
-
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
18
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
19
|
-
tabs: vi.fn().mockResolvedValue([]),
|
|
20
|
-
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
21
|
-
networkRequests: vi.fn().mockResolvedValue([]),
|
|
22
|
-
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
23
|
-
scroll: vi.fn().mockResolvedValue(undefined),
|
|
24
|
-
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
25
|
-
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
26
|
-
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
27
|
-
getCookies: vi.fn().mockResolvedValue([]),
|
|
28
|
-
screenshot: vi.fn().mockResolvedValue(''),
|
|
29
|
-
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
8
|
+
import { createPageMock } from '../test-utils.js';
|
|
32
9
|
describe('jd command registration', () => {
|
|
33
10
|
it('registers all jd shopping commands', () => {
|
|
34
11
|
for (const name of ['search', 'detail', 'reviews', 'cart', 'add-cart']) {
|