@jackwener/opencli 1.7.12 → 1.7.14
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 +12128 -6665
- 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/quote.js +139 -0
- package/clis/twitter/quote.test.js +106 -0
- package/clis/twitter/reply-dm.js +1 -1
- package/clis/twitter/reply.test.js +1 -29
- package/clis/twitter/retweet.js +99 -0
- package/clis/twitter/retweet.test.js +69 -0
- package/clis/twitter/shared.js +38 -0
- package/clis/twitter/shared.test.js +28 -1
- package/clis/twitter/unlike.js +87 -0
- package/clis/twitter/unlike.test.js +72 -0
- package/clis/twitter/unretweet.js +99 -0
- package/clis/twitter/unretweet.test.js +69 -0
- 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/bridge.js +47 -45
- 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/browser.test.js +18 -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 +241 -1
- package/dist/src/commanderAdapter.js +23 -9
- 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 +27 -2
- package/dist/src/help.js +196 -23
- 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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for 一亩三分地 (1point3acres.com) adapters.
|
|
3
|
+
*
|
|
4
|
+
* Site is a Discuz!X PHP BBS that serves GBK-encoded HTML.
|
|
5
|
+
* - Thread listings: /bbs/forum.php?mod=guide&view={hot|new|digest|newthread}
|
|
6
|
+
* - Forum: /bbs/forum-<fid>-<page>.html
|
|
7
|
+
* - Thread detail: /bbs/thread-<tid>-<page>-1.html
|
|
8
|
+
* - User profile: /bbs/space-uid-<uid>.html or /bbs/space-username-<name>.html
|
|
9
|
+
* - Search: /bbs/search.php?mod=forum (COOKIE — guests get an alert page)
|
|
10
|
+
*/
|
|
11
|
+
import { AuthRequiredError, ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
12
|
+
|
|
13
|
+
export const BASE = 'https://www.1point3acres.com/bbs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate `limit` per typed-fail-fast convention (no silent clamp).
|
|
17
|
+
* Throws ArgumentError on non-positive / non-integer / out-of-range input.
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeLimit(value, defaultValue, maxValue, label = 'limit') {
|
|
20
|
+
const limit = normalizePositiveInteger(value, defaultValue, label);
|
|
21
|
+
if (limit > maxValue) {
|
|
22
|
+
throw new ArgumentError(`${label} must be <= ${maxValue}`);
|
|
23
|
+
}
|
|
24
|
+
return limit;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Validate a positive integer argument without silently flooring/clamping. */
|
|
28
|
+
export function normalizePositiveInteger(value, defaultValue, label = 'value', { min = 1 } = {}) {
|
|
29
|
+
const raw = value ?? defaultValue;
|
|
30
|
+
const limit = Number(raw);
|
|
31
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
32
|
+
throw new ArgumentError(`${label} must be a positive integer`);
|
|
33
|
+
}
|
|
34
|
+
if (limit < min) {
|
|
35
|
+
throw new ArgumentError(`${label} must be >= ${min}`);
|
|
36
|
+
}
|
|
37
|
+
return limit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0 Safari/537.36';
|
|
41
|
+
|
|
42
|
+
/** Fetch a GBK-encoded Discuz page and return decoded UTF-8 HTML. */
|
|
43
|
+
export async function fetchHtml(url, { headers = {}, cookie = '' } = {}) {
|
|
44
|
+
let res;
|
|
45
|
+
try {
|
|
46
|
+
res = await fetch(url, {
|
|
47
|
+
headers: {
|
|
48
|
+
'User-Agent': UA,
|
|
49
|
+
'Accept': 'text/html,application/xhtml+xml',
|
|
50
|
+
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
51
|
+
...(cookie ? { Cookie: cookie } : {}),
|
|
52
|
+
...headers,
|
|
53
|
+
},
|
|
54
|
+
redirect: 'follow',
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new CommandExecutionError(`1point3acres request failed: ${error?.message || error}`);
|
|
58
|
+
}
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw new CommandExecutionError(`1point3acres request failed: HTTP ${res.status} ${res.statusText} from ${url}`);
|
|
61
|
+
}
|
|
62
|
+
const buf = await res.arrayBuffer();
|
|
63
|
+
return new TextDecoder('gbk').decode(buf);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Pull cookie string from the live browser session for this domain.
|
|
67
|
+
* Discuz auth cookies (4Oaf_61d6_*, session) are HttpOnly and set on the
|
|
68
|
+
* root domain `.1point3acres.com`, so we need `getCookies` (not document.cookie)
|
|
69
|
+
* AND we need to query both host + root domain and merge.
|
|
70
|
+
*/
|
|
71
|
+
export async function getCookie(page) {
|
|
72
|
+
if (!page) return '';
|
|
73
|
+
const seen = new Map();
|
|
74
|
+
if (typeof page.getCookies === 'function') {
|
|
75
|
+
for (const opts of [{ domain: 'www.1point3acres.com' }, { domain: '.1point3acres.com' }]) {
|
|
76
|
+
try {
|
|
77
|
+
const cookies = await page.getCookies(opts);
|
|
78
|
+
for (const c of cookies || []) {
|
|
79
|
+
if (!seen.has(c.name)) seen.set(c.name, c.value);
|
|
80
|
+
}
|
|
81
|
+
} catch { /* try next */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (seen.size > 0) {
|
|
85
|
+
return [...seen].map(([k, v]) => `${k}=${v}`).join('; ');
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const result = await page.evaluate('document.cookie');
|
|
89
|
+
return typeof result === 'string' ? result : '';
|
|
90
|
+
} catch {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Detect the "you are a guest" alert page that Discuz returns for protected actions. */
|
|
96
|
+
export function assertNotGuestAlert(html, domain = 'www.1point3acres.com') {
|
|
97
|
+
if (/<title>提示信息 \| 一亩三分地<\/title>/.test(html) && /无法进行此操作/.test(html)) {
|
|
98
|
+
throw new AuthRequiredError(domain, '需要登录一亩三分地后再使用该命令');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const ENTITY_MAP = {
|
|
103
|
+
' ': ' ', '&': '&', '<': '<', '>': '>',
|
|
104
|
+
'"': '"', ''': "'", ''': "'",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Decode HTML entities (numeric + common named). */
|
|
108
|
+
export function decodeEntities(s) {
|
|
109
|
+
if (!s) return '';
|
|
110
|
+
return s
|
|
111
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
|
|
112
|
+
.replace(/&#[xX]([0-9a-fA-F]+);/g, (_, n) => String.fromCodePoint(parseInt(n, 16)))
|
|
113
|
+
.replace(/&(nbsp|amp|lt|gt|quot|#39|apos);/g, m => ENTITY_MAP[m] || m);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Strip HTML tags and collapse whitespace, returning plain text. */
|
|
117
|
+
export function stripHtml(html) {
|
|
118
|
+
if (!html) return '';
|
|
119
|
+
return decodeEntities(
|
|
120
|
+
String(html)
|
|
121
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
122
|
+
.replace(/<\/(p|div|li|tr)>/gi, '\n')
|
|
123
|
+
.replace(/<[^>]+>/g, '')
|
|
124
|
+
).replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Truncate text to n characters with ellipsis. */
|
|
128
|
+
export function truncate(s, n = 300) {
|
|
129
|
+
if (!s) return '';
|
|
130
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Extract all <tbody id="normalthread_*"> blocks from a forum/guide page. */
|
|
134
|
+
export function parseThreadRows(html) {
|
|
135
|
+
const rows = [];
|
|
136
|
+
const re = /<tbody id="(normalthread|stickthread)_(\d+)"[^>]*>([\s\S]*?)<\/tbody>/g;
|
|
137
|
+
let m;
|
|
138
|
+
while ((m = re.exec(html))) {
|
|
139
|
+
const [, kind, tid, inner] = m;
|
|
140
|
+
rows.push({ kind, tid, inner });
|
|
141
|
+
}
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Parse a single Discuz thread row (inner HTML of the tbody). */
|
|
146
|
+
export function parseThreadRow({ kind, tid, inner }) {
|
|
147
|
+
const titleMatches = [...inner.matchAll(/<a [^>]*class="[^"]*\bxst\b[^"]*"[^>]*>([^<]+)<\/a>/g)];
|
|
148
|
+
const title = titleMatches.length
|
|
149
|
+
? decodeEntities(titleMatches[titleMatches.length - 1][1].trim())
|
|
150
|
+
: '';
|
|
151
|
+
|
|
152
|
+
const forumMatch = inner.match(/<a href="forum-(\d+)-1\.html"[^>]*target="_blank"[^>]*>([^<]+)<\/a>/);
|
|
153
|
+
const fid = forumMatch ? forumMatch[1] : '';
|
|
154
|
+
const forumName = forumMatch ? decodeEntities(forumMatch[2].trim()) : '';
|
|
155
|
+
|
|
156
|
+
// <td class="by"> blocks; first with <cite> = author, last with <cite> = last reply
|
|
157
|
+
const byBlocks = [...inner.matchAll(/<td class="by"[^>]*>([\s\S]*?)<\/td>/g)].map(m => m[1]);
|
|
158
|
+
const readCite = (block) => {
|
|
159
|
+
const m = block.match(/<cite[^>]*>([\s\S]*?)<\/cite>/);
|
|
160
|
+
if (!m) return '';
|
|
161
|
+
return decodeEntities(m[1].replace(/<[^>]+>/g, '').trim());
|
|
162
|
+
};
|
|
163
|
+
const readTime = (block) => {
|
|
164
|
+
const titleM = block.match(/<span [^>]*title="([^"]+)"[^>]*>/);
|
|
165
|
+
if (titleM) return titleM[1].trim();
|
|
166
|
+
const plainA = block.match(/<em>[\s\S]*?<a [^>]*>\s*([^<]+?)\s*<\/a>/);
|
|
167
|
+
if (plainA) return decodeEntities(plainA[1].trim());
|
|
168
|
+
const plainSpan = block.match(/<em>[\s\S]*?<span[^>]*>\s*([^<]+?)\s*<\/span>/);
|
|
169
|
+
if (plainSpan) return decodeEntities(plainSpan[1].trim());
|
|
170
|
+
const bare = block.match(/<em>\s*([^<]+?)\s*<\/em>/);
|
|
171
|
+
return bare ? decodeEntities(bare[1].trim()) : '';
|
|
172
|
+
};
|
|
173
|
+
let authorBlock = '';
|
|
174
|
+
let lastBlock = '';
|
|
175
|
+
for (const b of byBlocks) {
|
|
176
|
+
if (/<cite/.test(b)) {
|
|
177
|
+
if (!authorBlock) authorBlock = b;
|
|
178
|
+
lastBlock = b;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const author = authorBlock ? readCite(authorBlock) : '';
|
|
182
|
+
const postTime = authorBlock ? readTime(authorBlock) : '';
|
|
183
|
+
const lastReplyUser = lastBlock && lastBlock !== authorBlock ? readCite(lastBlock) : '';
|
|
184
|
+
const lastReplyTime = lastBlock && lastBlock !== authorBlock ? readTime(lastBlock) : '';
|
|
185
|
+
|
|
186
|
+
const numMatch = inner.match(/<td class="num"[^>]*>\s*<a[^>]*class="xi2"[^>]*>(\d+)<\/a>(?:\s*<em>(\d+)<\/em>)?/);
|
|
187
|
+
const replies = numMatch ? Number(numMatch[1]) : 0;
|
|
188
|
+
const views = numMatch && numMatch[2] ? Number(numMatch[2]) : 0;
|
|
189
|
+
return {
|
|
190
|
+
tid,
|
|
191
|
+
kind,
|
|
192
|
+
title,
|
|
193
|
+
author,
|
|
194
|
+
forum: forumName,
|
|
195
|
+
fid,
|
|
196
|
+
replies,
|
|
197
|
+
views,
|
|
198
|
+
postTime,
|
|
199
|
+
lastReplyUser,
|
|
200
|
+
lastReplyTime,
|
|
201
|
+
url: `${BASE}/thread-${tid}-1-1.html`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Quick one-shot listing parser used by hot/latest/digest/forum. */
|
|
206
|
+
export function parseThreadList(html) {
|
|
207
|
+
return parseThreadRows(html).map(parseThreadRow).filter(t => t.title);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Parse Discuz search results page (different HTML shape than forum listings).
|
|
212
|
+
* Each hit is <li class="pbw" id="TID"> containing h3 > a[href*="tid=TID"],
|
|
213
|
+
* <p class="xg1">N 个回复 - M 次查看</p>, and a time/author/forum <p>.
|
|
214
|
+
*/
|
|
215
|
+
export function parseSearchList(html) {
|
|
216
|
+
const items = [];
|
|
217
|
+
const re = /<li class="pbw" id="(\d+)">([\s\S]*?)<\/li>/g;
|
|
218
|
+
let m;
|
|
219
|
+
while ((m = re.exec(html))) {
|
|
220
|
+
const [, tid, inner] = m;
|
|
221
|
+
const titleMatch = inner.match(/<h3[^>]*>\s*<a [^>]*>([\s\S]*?)<\/a>/);
|
|
222
|
+
const titleRaw = titleMatch ? titleMatch[1] : '';
|
|
223
|
+
const title = decodeEntities(titleRaw.replace(/<[^>]+>/g, '')).trim();
|
|
224
|
+
if (!title) continue;
|
|
225
|
+
|
|
226
|
+
const statsMatch = inner.match(/<p class="xg1">\s*([\d,]+)\s*个回复\s*-\s*([\d,]+)\s*次查看\s*<\/p>/);
|
|
227
|
+
const replies = statsMatch ? Number(statsMatch[1].replace(/,/g, '')) : 0;
|
|
228
|
+
const views = statsMatch ? Number(statsMatch[2].replace(/,/g, '')) : 0;
|
|
229
|
+
|
|
230
|
+
const metaMatch = inner.match(/<p>\s*<span>([^<]+)<\/span>[\s\S]*?<a [^>]*space-uid-\d+[^>]*>([^<]+?)<\/a>[\s\S]*?<a [^>]*href="forum-(\d+)-[^"]*"[^>]*>([^<]+?)<\/a>/);
|
|
231
|
+
const postTime = metaMatch ? decodeEntities(metaMatch[1].trim()) : '';
|
|
232
|
+
const author = metaMatch ? decodeEntities(metaMatch[2].trim()) : '';
|
|
233
|
+
const fid = metaMatch ? metaMatch[3] : '';
|
|
234
|
+
const forumName = metaMatch ? decodeEntities(metaMatch[4].trim()) : '';
|
|
235
|
+
|
|
236
|
+
items.push({
|
|
237
|
+
tid, title, author, forum: forumName, fid,
|
|
238
|
+
replies, views, postTime,
|
|
239
|
+
// Search pages don't show lastReplyTime separately — surface postTime instead.
|
|
240
|
+
lastReplyUser: '', lastReplyTime: postTime,
|
|
241
|
+
url: `${BASE}/thread-${tid}-1-1.html`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return items;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { UA };
|
|
@@ -14,6 +14,7 @@ export function makeScreenshotCommand(site, displayName, extra = {}) {
|
|
|
14
14
|
...extra,
|
|
15
15
|
site,
|
|
16
16
|
name: 'screenshot',
|
|
17
|
+
access: 'read',
|
|
17
18
|
description: `Capture a snapshot of the current ${label} window (DOM + Accessibility tree)`,
|
|
18
19
|
domain: 'localhost',
|
|
19
20
|
strategy: Strategy.UI,
|
|
@@ -46,6 +47,7 @@ export function makeStatusCommand(site, displayName, extra = {}) {
|
|
|
46
47
|
...extra,
|
|
47
48
|
site,
|
|
48
49
|
name: 'status',
|
|
50
|
+
access: 'read',
|
|
49
51
|
description: `Check active CDP connection to ${label}`,
|
|
50
52
|
domain: 'localhost',
|
|
51
53
|
strategy: Strategy.UI,
|
|
@@ -67,6 +69,7 @@ export function makeNewCommand(site, displayName, extra = {}) {
|
|
|
67
69
|
...extra,
|
|
68
70
|
site,
|
|
69
71
|
name: 'new',
|
|
72
|
+
access: 'write',
|
|
70
73
|
description: `Start a new ${label} session`,
|
|
71
74
|
domain: 'localhost',
|
|
72
75
|
strategy: Strategy.UI,
|
|
@@ -87,6 +90,7 @@ export function makeDumpCommand(site) {
|
|
|
87
90
|
return cli({
|
|
88
91
|
site,
|
|
89
92
|
name: 'dump',
|
|
93
|
+
access: 'read',
|
|
90
94
|
description: `Dump the DOM and Accessibility tree of ${site} for reverse-engineering`,
|
|
91
95
|
domain: 'localhost',
|
|
92
96
|
strategy: Strategy.UI,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError, getErrorMessage } from '@jackwener/opencli/errors';
|
|
3
|
+
|
|
4
|
+
const AIBASE_DAILY_URL = 'https://www.aibase.com/zh/daily';
|
|
5
|
+
const DEFAULT_LIMIT = 20;
|
|
6
|
+
const MAX_LIMIT = 50;
|
|
7
|
+
|
|
8
|
+
function normalizeLimit(value) {
|
|
9
|
+
const raw = value ?? DEFAULT_LIMIT;
|
|
10
|
+
const limit = Number(raw);
|
|
11
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
12
|
+
throw new ArgumentError('limit must be a positive integer', `Example: opencli aibase news --limit ${DEFAULT_LIMIT}`);
|
|
13
|
+
}
|
|
14
|
+
if (limit > MAX_LIMIT) {
|
|
15
|
+
throw new ArgumentError(`limit must be <= ${MAX_LIMIT}`, `Example: opencli aibase news --limit ${MAX_LIMIT}`);
|
|
16
|
+
}
|
|
17
|
+
return limit;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeText(value) {
|
|
21
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildExtractAibaseNewsJs() {
|
|
25
|
+
return `
|
|
26
|
+
(() => {
|
|
27
|
+
const anchors = Array.from(document.querySelectorAll('.bg-white .grid a[href], a[href*="/zh/daily/"]'))
|
|
28
|
+
.filter((anchor) => {
|
|
29
|
+
const href = anchor.getAttribute('href') || '';
|
|
30
|
+
const text = (anchor.innerText || anchor.textContent || '').trim();
|
|
31
|
+
return text && href && !href.endsWith('/zh/daily') && !href.endsWith('/zh/daily/');
|
|
32
|
+
});
|
|
33
|
+
if (anchors.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
reason: 'selector-missing',
|
|
37
|
+
title: document.title || '',
|
|
38
|
+
bodyText: (document.body?.innerText || document.body?.textContent || '').slice(0, 500),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const rows = [];
|
|
43
|
+
for (const anchor of anchors) {
|
|
44
|
+
const url = new URL(anchor.getAttribute('href'), location.href).href;
|
|
45
|
+
if (seen.has(url)) continue;
|
|
46
|
+
seen.add(url);
|
|
47
|
+
rows.push({
|
|
48
|
+
rank: rows.length + 1,
|
|
49
|
+
title: anchor.innerText || anchor.textContent || '',
|
|
50
|
+
url,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, rows };
|
|
54
|
+
})()
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toRows(payload, limit) {
|
|
59
|
+
if (!payload || typeof payload !== 'object') {
|
|
60
|
+
throw new CommandExecutionError('AIbase daily page returned an unreadable payload');
|
|
61
|
+
}
|
|
62
|
+
if (!payload.ok) {
|
|
63
|
+
const reason = typeof payload.reason === 'string' && payload.reason.trim() ? payload.reason.trim() : 'selector-drift';
|
|
64
|
+
throw new CommandExecutionError(
|
|
65
|
+
`AIbase daily selector drift: ${reason}`,
|
|
66
|
+
payload.title ? `Page title: ${payload.title}` : undefined,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const rows = (Array.isArray(payload.rows) ? payload.rows : [])
|
|
70
|
+
.map((row, index) => ({
|
|
71
|
+
rank: index + 1,
|
|
72
|
+
title: normalizeText(row.title),
|
|
73
|
+
url: normalizeText(row.url),
|
|
74
|
+
}))
|
|
75
|
+
.filter((row) => row.title && row.url);
|
|
76
|
+
if (rows.length === 0) {
|
|
77
|
+
throw new EmptyResultError('aibase news', 'AIbase daily page loaded, but no article rows with title and URL were extracted.');
|
|
78
|
+
}
|
|
79
|
+
return rows.slice(0, limit).map((row, index) => ({ ...row, rank: index + 1 }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function loadAibaseNews(page, args) {
|
|
83
|
+
const limit = normalizeLimit(args.limit);
|
|
84
|
+
await page.goto(AIBASE_DAILY_URL, { waitUntil: 'load', settleMs: 3000 });
|
|
85
|
+
const payload = await page.evaluate(buildExtractAibaseNewsJs()).catch((error) => {
|
|
86
|
+
throw new CommandExecutionError(`Failed to extract AIbase daily news: ${getErrorMessage(error)}`);
|
|
87
|
+
});
|
|
88
|
+
return toRows(payload, limit);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const aibaseNewsCommand = cli({
|
|
92
|
+
site: 'aibase',
|
|
93
|
+
name: 'news',
|
|
94
|
+
access: 'read',
|
|
95
|
+
description: 'AIbase 日报 - 每天三分钟关注AI行业趋势',
|
|
96
|
+
domain: 'www.aibase.com',
|
|
97
|
+
strategy: Strategy.PUBLIC,
|
|
98
|
+
browser: true,
|
|
99
|
+
args: [
|
|
100
|
+
{ name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of news items to return (max ${MAX_LIMIT})` },
|
|
101
|
+
],
|
|
102
|
+
columns: ['rank', 'title', 'url'],
|
|
103
|
+
func: loadAibaseNews,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export const __test__ = {
|
|
107
|
+
buildExtractAibaseNewsJs,
|
|
108
|
+
normalizeLimit,
|
|
109
|
+
toRows,
|
|
110
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
import { aibaseNewsCommand, __test__ } from './news.js';
|
|
5
|
+
|
|
6
|
+
function runBrowserScript(html, script, url = 'https://www.aibase.com/zh/daily') {
|
|
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('aibase/news', () => {
|
|
19
|
+
it('registers stable URL in columns', () => {
|
|
20
|
+
expect(aibaseNewsCommand.access).toBe('read');
|
|
21
|
+
expect(aibaseNewsCommand.columns).toEqual(['rank', 'title', 'url']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('validates limit before browser navigation', async () => {
|
|
25
|
+
const page = makePage({ ok: true, rows: [] });
|
|
26
|
+
await expect(aibaseNewsCommand.func(page, { limit: 0 })).rejects.toBeInstanceOf(ArgumentError);
|
|
27
|
+
await expect(aibaseNewsCommand.func(page, { limit: 51 })).rejects.toBeInstanceOf(ArgumentError);
|
|
28
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('extracts and deduplicates AIbase daily rows', async () => {
|
|
32
|
+
const html = `
|
|
33
|
+
<div class="bg-white">
|
|
34
|
+
<div class="grid">
|
|
35
|
+
<a href="/zh/daily/123"> First AI daily item </a>
|
|
36
|
+
<a href="/zh/daily/123"> First AI daily item duplicate </a>
|
|
37
|
+
<a href="/zh/daily/456"> Second AI daily item </a>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
`;
|
|
41
|
+
const payload = runBrowserScript(html, __test__.buildExtractAibaseNewsJs());
|
|
42
|
+
const page = makePage(payload);
|
|
43
|
+
|
|
44
|
+
const rows = await aibaseNewsCommand.func(page, { limit: 2 });
|
|
45
|
+
|
|
46
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.aibase.com/zh/daily', { waitUntil: 'load', settleMs: 3000 });
|
|
47
|
+
expect(rows).toEqual([
|
|
48
|
+
{ rank: 1, title: 'First AI daily item', url: 'https://www.aibase.com/zh/daily/123' },
|
|
49
|
+
{ rank: 2, title: 'Second AI daily item', url: 'https://www.aibase.com/zh/daily/456' },
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('maps selector drift and empty rows to typed errors', async () => {
|
|
54
|
+
await expect(aibaseNewsCommand.func(makePage({ ok: false, reason: 'selector-missing' }), { limit: 1 }))
|
|
55
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
56
|
+
await expect(aibaseNewsCommand.func(makePage({ ok: true, rows: [{ title: '', url: '' }] }), { limit: 1 }))
|
|
57
|
+
.rejects.toBeInstanceOf(EmptyResultError);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -3,35 +3,8 @@ import { AuthRequiredError } from '@jackwener/opencli/errors';
|
|
|
3
3
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
4
|
import { __test__ } from './discussion.js';
|
|
5
5
|
import './discussion.js';
|
|
6
|
+
import { createPageMock } from '../test-utils.js';
|
|
6
7
|
|
|
7
|
-
function createPageMock(evaluateResults) {
|
|
8
|
-
const evaluate = vi.fn();
|
|
9
|
-
for (const result of evaluateResults) {
|
|
10
|
-
evaluate.mockResolvedValueOnce(result);
|
|
11
|
-
}
|
|
12
|
-
return {
|
|
13
|
-
goto: vi.fn().mockResolvedValue(undefined),
|
|
14
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
15
|
-
evaluate,
|
|
16
|
-
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
17
|
-
click: vi.fn().mockResolvedValue(undefined),
|
|
18
|
-
typeText: vi.fn().mockResolvedValue(undefined),
|
|
19
|
-
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
20
|
-
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
21
|
-
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
22
|
-
tabs: vi.fn().mockResolvedValue([]),
|
|
23
|
-
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
24
|
-
networkRequests: vi.fn().mockResolvedValue([]),
|
|
25
|
-
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
26
|
-
scroll: vi.fn().mockResolvedValue(undefined),
|
|
27
|
-
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
28
|
-
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
29
|
-
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
30
|
-
getCookies: vi.fn().mockResolvedValue([]),
|
|
31
|
-
screenshot: vi.fn().mockResolvedValue(''),
|
|
32
|
-
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
8
|
|
|
36
9
|
describe('amazon discussion normalization', () => {
|
|
37
10
|
it('normalizes review summary and sample reviews', () => {
|
|
@@ -7,8 +7,9 @@ export const watchCommand = cli({
|
|
|
7
7
|
domain: 'localhost',
|
|
8
8
|
strategy: Strategy.UI,
|
|
9
9
|
browser: true,
|
|
10
|
-
args: [
|
|
11
|
-
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'timeout', type: 'int', required: false, default: 86400, help: 'Max seconds to keep watching (default: 86400 — 24h)' },
|
|
12
|
+
],
|
|
12
13
|
columns: [], // We use direct stdout streaming
|
|
13
14
|
func: async (page) => {
|
|
14
15
|
console.log('Watching Antigravity chat... (Press Ctrl+C to stop)');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// arxiv author — list papers authored by a person, newest first.
|
|
2
|
+
//
|
|
3
|
+
// arXiv's public API supports `au:` prefix queries. Author names on arXiv are
|
|
4
|
+
// not stable IDs, so this is a best-effort fuzzy match — the same person can
|
|
5
|
+
// appear under multiple spellings ("Y. Bengio" vs "Yoshua Bengio").
|
|
6
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
8
|
+
import { arxivFetch, normalizeArxivLimit, parseEntries } from './utils.js';
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'arxiv',
|
|
12
|
+
name: 'author',
|
|
13
|
+
access: 'read',
|
|
14
|
+
description: 'List arXiv papers by a given author (newest first)',
|
|
15
|
+
strategy: Strategy.PUBLIC,
|
|
16
|
+
browser: false,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'author', positional: true, required: true, help: 'Author name (e.g. "Yoshua Bengio" or "Y Bengio")' },
|
|
19
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max papers to return (max 50)' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['id', 'title', 'authors', 'published', 'primary_category', 'url'],
|
|
22
|
+
func: async (args) => {
|
|
23
|
+
const authorText = String(args.author || '').trim();
|
|
24
|
+
if (!authorText) {
|
|
25
|
+
throw new ArgumentError('arxiv author cannot be empty', 'Example: opencli arxiv author "Yoshua Bengio"');
|
|
26
|
+
}
|
|
27
|
+
const limit = normalizeArxivLimit(args.limit, 20, 50);
|
|
28
|
+
// Quote the value so multi-word author names match as a phrase.
|
|
29
|
+
const query = encodeURIComponent(`au:"${authorText}"`);
|
|
30
|
+
const xml = await arxivFetch(`search_query=${query}&max_results=${limit}&sortBy=submittedDate&sortOrder=descending`);
|
|
31
|
+
const entries = parseEntries(xml);
|
|
32
|
+
if (!entries.length) {
|
|
33
|
+
throw new EmptyResultError('arxiv author', `No papers found for author "${authorText}". Try alternate spellings (e.g. initials).`);
|
|
34
|
+
}
|
|
35
|
+
return entries.map(e => ({
|
|
36
|
+
id: e.id,
|
|
37
|
+
title: e.title,
|
|
38
|
+
authors: e.authors,
|
|
39
|
+
published: e.published,
|
|
40
|
+
primary_category: e.primary_category,
|
|
41
|
+
url: e.url,
|
|
42
|
+
}));
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -14,7 +14,6 @@ cli({
|
|
|
14
14
|
{ name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
|
|
15
15
|
],
|
|
16
16
|
columns: ['rank', 'title', 'authors', 'journal', 'year', '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);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// bbc topic — BBC News headlines for a specific category, via public RSS.
|
|
2
|
+
//
|
|
3
|
+
// BBC publishes per-section RSS feeds at
|
|
4
|
+
// `https://feeds.bbci.co.uk/news/<topic>/rss.xml`. We expose the eight
|
|
5
|
+
// canonical sections and reject anything else with a typed argument error
|
|
6
|
+
// so the user knows the supported set.
|
|
7
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
8
|
+
import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
9
|
+
import { bbcFetchRss, parseRssItems, pubDateToIso, requireBoundedInt } from './utils.js';
|
|
10
|
+
|
|
11
|
+
const TOPICS = [
|
|
12
|
+
'world',
|
|
13
|
+
'business',
|
|
14
|
+
'politics',
|
|
15
|
+
'health',
|
|
16
|
+
'education',
|
|
17
|
+
'science_and_environment',
|
|
18
|
+
'technology',
|
|
19
|
+
'entertainment_and_arts',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
cli({
|
|
23
|
+
site: 'bbc',
|
|
24
|
+
name: 'topic',
|
|
25
|
+
access: 'read',
|
|
26
|
+
description: 'BBC News headlines for a specific section (RSS feed)',
|
|
27
|
+
domain: 'www.bbc.com',
|
|
28
|
+
strategy: Strategy.PUBLIC,
|
|
29
|
+
browser: false,
|
|
30
|
+
args: [
|
|
31
|
+
{ name: 'topic', positional: true, required: true, help: `Section name (${TOPICS.join(' / ')})` },
|
|
32
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max headlines (1-50)' },
|
|
33
|
+
],
|
|
34
|
+
columns: ['rank', 'title', 'description', 'pubDate', 'url'],
|
|
35
|
+
func: async (args) => {
|
|
36
|
+
const raw = String(args.topic ?? '').trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
37
|
+
if (!TOPICS.includes(raw)) {
|
|
38
|
+
throw new ArgumentError(
|
|
39
|
+
`bbc topic "${args.topic}" is not supported`,
|
|
40
|
+
`Supported topics: ${TOPICS.join(', ')}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const limit = requireBoundedInt(args.limit, 20, 50);
|
|
44
|
+
const xml = await bbcFetchRss(`${raw}/rss.xml`, `bbc topic ${raw}`);
|
|
45
|
+
const items = parseRssItems(xml);
|
|
46
|
+
if (!items.length) {
|
|
47
|
+
throw new EmptyResultError('bbc topic', `BBC ${raw} feed returned no items.`);
|
|
48
|
+
}
|
|
49
|
+
return items.slice(0, limit).map((it, i) => ({
|
|
50
|
+
rank: i + 1,
|
|
51
|
+
title: it.title,
|
|
52
|
+
description: it.description,
|
|
53
|
+
pubDate: pubDateToIso(it.pubDate),
|
|
54
|
+
url: it.link,
|
|
55
|
+
}));
|
|
56
|
+
},
|
|
57
|
+
});
|