@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,370 @@
|
|
|
1
|
+
// Contract tests for the Phase-3 P0.5 refactor that retired the legacy
|
|
2
|
+
// pipeline-based silent-failure pattern across comment / follow / unfollow.
|
|
3
|
+
//
|
|
4
|
+
// Each adapter is now `func + Strategy.COOKIE + browser:true` with a
|
|
5
|
+
// shared button-walker helper bundle from utils.js. These tests pin
|
|
6
|
+
// down (a) registration metadata, (b) typed-error boundary, including
|
|
7
|
+
// the retryable-hint contract that distinguishes server-fan-out
|
|
8
|
+
// (comment) from idempotent client-safe (follow / unfollow) failures,
|
|
9
|
+
// (c) build-script invariants — sentinels, login + rate-limit guards,
|
|
10
|
+
// expected button labels and selectors.
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
ArgumentError,
|
|
15
|
+
AuthRequiredError,
|
|
16
|
+
CommandExecutionError,
|
|
17
|
+
} from '@jackwener/opencli/errors';
|
|
18
|
+
import { commentCommand, __test__ as commentTest } from './comment.js';
|
|
19
|
+
import { followCommand, __test__ as followTest } from './follow.js';
|
|
20
|
+
import { unfollowCommand, __test__ as unfollowTest } from './unfollow.js';
|
|
21
|
+
import {
|
|
22
|
+
BUTTON_WALKER_HELPERS,
|
|
23
|
+
BUTTON_WALKER_SENTINELS,
|
|
24
|
+
COMMENT_TEXT_MAX,
|
|
25
|
+
RETRYABLE_HINTS,
|
|
26
|
+
parseTikTokVideoUrl,
|
|
27
|
+
requireCommentText,
|
|
28
|
+
throwButtonWalkerError,
|
|
29
|
+
} from './utils.js';
|
|
30
|
+
|
|
31
|
+
function makePage(rows) {
|
|
32
|
+
return {
|
|
33
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
evaluate: vi.fn().mockResolvedValue(rows),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeFailingPage(error) {
|
|
40
|
+
return {
|
|
41
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
evaluate: vi.fn().mockRejectedValue(error),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeGotoFailingPage(error) {
|
|
48
|
+
return {
|
|
49
|
+
goto: vi.fn().mockRejectedValue(error),
|
|
50
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
51
|
+
evaluate: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const VIDEO_URL = 'https://www.tiktok.com/@creator/video/7350000000000000000';
|
|
56
|
+
|
|
57
|
+
const sampleCommentRow = { url: VIDEO_URL, text: 'great clip', result: 'posted' };
|
|
58
|
+
const sampleFollowRow = { username: 'creator', url: 'https://www.tiktok.com/@creator', result: 'followed' };
|
|
59
|
+
const sampleUnfollowRow = { username: 'creator', url: 'https://www.tiktok.com/@creator', result: 'unfollowed' };
|
|
60
|
+
|
|
61
|
+
describe('tiktok/utils (P0.5 button-walker additions)', () => {
|
|
62
|
+
it('requireCommentText rejects empty / whitespace / overlong with ArgumentError', () => {
|
|
63
|
+
expect(() => requireCommentText('')).toThrow(ArgumentError);
|
|
64
|
+
expect(() => requireCommentText(' ')).toThrow(ArgumentError);
|
|
65
|
+
expect(() => requireCommentText(undefined)).toThrow(ArgumentError);
|
|
66
|
+
expect(() => requireCommentText('x'.repeat(COMMENT_TEXT_MAX + 1))).toThrow(ArgumentError);
|
|
67
|
+
expect(requireCommentText(' hello ')).toBe('hello');
|
|
68
|
+
expect(requireCommentText('x'.repeat(COMMENT_TEXT_MAX))).toHaveLength(COMMENT_TEXT_MAX);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('parseTikTokVideoUrl rejects non-tiktok / bad shape and accepts canonical URL', () => {
|
|
72
|
+
expect(() => parseTikTokVideoUrl('')).toThrow(ArgumentError);
|
|
73
|
+
expect(() => parseTikTokVideoUrl('not a url')).toThrow(ArgumentError);
|
|
74
|
+
expect(() => parseTikTokVideoUrl('https://example.com/@u/video/123')).toThrow(ArgumentError);
|
|
75
|
+
expect(() => parseTikTokVideoUrl('https://www.tiktok.com/@user/photo/123')).toThrow(ArgumentError);
|
|
76
|
+
expect(() => parseTikTokVideoUrl('https://www.tiktok.com/@user/video/123abc')).toThrow(ArgumentError);
|
|
77
|
+
expect(() => parseTikTokVideoUrl('https://www.tiktok.com/@user/video/123/extra')).toThrow(ArgumentError);
|
|
78
|
+
expect(() => parseTikTokVideoUrl('https://vm.tiktok.com/abc')).toThrow(ArgumentError);
|
|
79
|
+
const parsed = parseTikTokVideoUrl(VIDEO_URL);
|
|
80
|
+
expect(parsed.username).toBe('creator');
|
|
81
|
+
expect(parsed.videoId).toBe('7350000000000000000');
|
|
82
|
+
expect(parsed.url).toBe(VIDEO_URL);
|
|
83
|
+
// accepts a trailing slash plus query string, but not extra path segments
|
|
84
|
+
expect(parseTikTokVideoUrl(`${VIDEO_URL}/?lang=en`).videoId).toBe('7350000000000000000');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('BUTTON_WALKER_SENTINELS exposes the 4 sentinel strings used by IIFEs', () => {
|
|
88
|
+
expect(BUTTON_WALKER_SENTINELS.AUTH_REQUIRED).toBe('AUTH_REQUIRED');
|
|
89
|
+
expect(BUTTON_WALKER_SENTINELS.BUTTON_NOT_FOUND).toBe('BUTTON_NOT_FOUND');
|
|
90
|
+
expect(BUTTON_WALKER_SENTINELS.STATE_VERIFY_FAIL).toBe('STATE_VERIFY_FAIL');
|
|
91
|
+
expect(BUTTON_WALKER_SENTINELS.RATE_LIMITED).toBe('RATE_LIMITED');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('RETRYABLE_HINTS encodes retryable=true|false plus reason in human-readable form', () => {
|
|
95
|
+
expect(RETRYABLE_HINTS.commentFailure).toMatch(/retryable=false/);
|
|
96
|
+
expect(RETRYABLE_HINTS.commentFailure).toMatch(/server-fan-out/);
|
|
97
|
+
expect(RETRYABLE_HINTS.relationFailure).toMatch(/retryable=true/);
|
|
98
|
+
expect(RETRYABLE_HINTS.relationFailure).toMatch(/idempotent/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('BUTTON_WALKER_HELPERS string template exposes the expected helper names', () => {
|
|
102
|
+
expect(typeof BUTTON_WALKER_HELPERS).toBe('string');
|
|
103
|
+
for (const name of [
|
|
104
|
+
'checkLoggedIn',
|
|
105
|
+
'findButtonByText',
|
|
106
|
+
'buttonExists',
|
|
107
|
+
'detectRateLimitPopup',
|
|
108
|
+
'waitFor',
|
|
109
|
+
'ensureLoggedInOrThrow',
|
|
110
|
+
'ensureNoRateLimitOrThrow',
|
|
111
|
+
]) {
|
|
112
|
+
expect(BUTTON_WALKER_HELPERS).toContain('function ' + name + '(');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('throwButtonWalkerError maps AUTH_REQUIRED-shaped messages to AuthRequiredError', () => {
|
|
117
|
+
expect(() => throwButtonWalkerError(new Error('AUTH_REQUIRED: login required'), {
|
|
118
|
+
authMessage: 'login pls',
|
|
119
|
+
failureMessage: 'op failed',
|
|
120
|
+
retryableHint: RETRYABLE_HINTS.relationFailure,
|
|
121
|
+
})).toThrow(AuthRequiredError);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('throwButtonWalkerError keeps captcha / rate-limit as retryable CommandExecutionError, not AuthRequiredError', () => {
|
|
125
|
+
for (const message of [
|
|
126
|
+
'RATE_LIMITED: TikTok rate limit / captcha detected',
|
|
127
|
+
'captcha verification needed',
|
|
128
|
+
'Too many requests, try again later',
|
|
129
|
+
]) {
|
|
130
|
+
try {
|
|
131
|
+
throwButtonWalkerError(new Error(message), {
|
|
132
|
+
authMessage: 'login pls',
|
|
133
|
+
failureMessage: 'op failed',
|
|
134
|
+
retryableHint: RETRYABLE_HINTS.relationFailure,
|
|
135
|
+
});
|
|
136
|
+
throw new Error('should have thrown');
|
|
137
|
+
} catch (err) {
|
|
138
|
+
expect(err).toBeInstanceOf(CommandExecutionError);
|
|
139
|
+
expect(err.hint).toMatch(/retryable=true/);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('throwButtonWalkerError maps everything else to CommandExecutionError with retryable hint', () => {
|
|
145
|
+
try {
|
|
146
|
+
throwButtonWalkerError(new Error('BUTTON_NOT_FOUND: missing'), {
|
|
147
|
+
authMessage: 'a',
|
|
148
|
+
failureMessage: 'op failed',
|
|
149
|
+
retryableHint: RETRYABLE_HINTS.commentFailure,
|
|
150
|
+
});
|
|
151
|
+
throw new Error('should have thrown');
|
|
152
|
+
} catch (err) {
|
|
153
|
+
expect(err).toBeInstanceOf(CommandExecutionError);
|
|
154
|
+
expect(err.message).toMatch(/op failed: BUTTON_NOT_FOUND/);
|
|
155
|
+
expect(err.hint).toMatch(/retryable=false/);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
throwButtonWalkerError(new Error('STATE_VERIFY_FAIL: did not flip'), {
|
|
159
|
+
authMessage: 'login pls',
|
|
160
|
+
failureMessage: 'op failed',
|
|
161
|
+
retryableHint: RETRYABLE_HINTS.relationFailure,
|
|
162
|
+
});
|
|
163
|
+
throw new Error('should have thrown');
|
|
164
|
+
} catch (err) {
|
|
165
|
+
expect(err).toBeInstanceOf(CommandExecutionError);
|
|
166
|
+
expect(err.hint).toMatch(/retryable=true/);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('tiktok/comment (Route 1 button-walker refactor)', () => {
|
|
172
|
+
it('registers as write-access COOKIE browser adapter with url/text/result columns', () => {
|
|
173
|
+
expect(commentCommand.access).toBe('write');
|
|
174
|
+
expect(commentCommand.browser).toBe(true);
|
|
175
|
+
expect(commentCommand.strategy).toBe('cookie');
|
|
176
|
+
expect(commentCommand.columns).toEqual(['url', 'text', 'result']);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('validates --url and --text upfront before navigating', async () => {
|
|
180
|
+
const page = makePage([sampleCommentRow]);
|
|
181
|
+
await expect(commentCommand.func(page, { url: '', text: 'hi' })).rejects.toBeInstanceOf(ArgumentError);
|
|
182
|
+
await expect(commentCommand.func(page, { url: VIDEO_URL, text: '' })).rejects.toBeInstanceOf(ArgumentError);
|
|
183
|
+
await expect(commentCommand.func(page, { url: 'https://example.com/x', text: 'hi' })).rejects.toBeInstanceOf(ArgumentError);
|
|
184
|
+
await expect(commentCommand.func(page, { url: VIDEO_URL, text: 'x'.repeat(COMMENT_TEXT_MAX + 1) })).rejects.toBeInstanceOf(ArgumentError);
|
|
185
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('navigates to the canonical video URL and returns the row from evaluate', async () => {
|
|
189
|
+
const page = makePage([sampleCommentRow]);
|
|
190
|
+
const rows = await commentCommand.func(page, { url: VIDEO_URL, text: 'great clip' });
|
|
191
|
+
expect(page.goto).toHaveBeenCalledWith(VIDEO_URL, { waitUntil: 'load', settleMs: 6000 });
|
|
192
|
+
expect(rows).toEqual([sampleCommentRow]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('maps page-evaluate errors to typed errors with comment retryable=false hint', async () => {
|
|
196
|
+
await expect(commentCommand.func(
|
|
197
|
+
makeFailingPage(new Error('AUTH_REQUIRED: TikTok login required')),
|
|
198
|
+
{ url: VIDEO_URL, text: 'hi' },
|
|
199
|
+
)).rejects.toBeInstanceOf(AuthRequiredError);
|
|
200
|
+
try {
|
|
201
|
+
await commentCommand.func(
|
|
202
|
+
makeFailingPage(new Error('STATE_VERIFY_FAIL: comment count did not increase')),
|
|
203
|
+
{ url: VIDEO_URL, text: 'hi' },
|
|
204
|
+
);
|
|
205
|
+
throw new Error('should have thrown');
|
|
206
|
+
} catch (err) {
|
|
207
|
+
expect(err).toBeInstanceOf(CommandExecutionError);
|
|
208
|
+
expect(err.message).toMatch(/Failed to post comment/);
|
|
209
|
+
expect(err.hint).toMatch(/retryable=false/);
|
|
210
|
+
expect(err.hint).toMatch(/server-fan-out/);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('maps navigation failures and empty evaluate rows to CommandExecutionError', async () => {
|
|
215
|
+
await expect(commentCommand.func(
|
|
216
|
+
makeGotoFailingPage(new Error('net::ERR_ABORTED')),
|
|
217
|
+
{ url: VIDEO_URL, text: 'hi' },
|
|
218
|
+
)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
219
|
+
await expect(commentCommand.func(
|
|
220
|
+
makePage([]),
|
|
221
|
+
{ url: VIDEO_URL, text: 'hi' },
|
|
222
|
+
)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('build script embeds text via JSON.stringify and calls login + rate-limit guards', () => {
|
|
226
|
+
const script = commentTest.buildCommentScript('he said "hi"\nbye');
|
|
227
|
+
expect(script).toContain('const commentText = "he said \\"hi\\"\\nbye";');
|
|
228
|
+
expect(script).toContain('ensureLoggedInOrThrow()');
|
|
229
|
+
expect(script).toContain('ensureNoRateLimitOrThrow()');
|
|
230
|
+
expect(script).toContain('STATE_VERIFY_FAIL');
|
|
231
|
+
expect(script).toContain('BUTTON_NOT_FOUND');
|
|
232
|
+
expect(script).toContain('[data-e2e="comment-input"]');
|
|
233
|
+
expect(script).toContain('[data-e2e="comment-level-1"]');
|
|
234
|
+
expect(script).toContain("['Post', '发布', '发送']");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('tiktok/follow (Route 1 button-walker refactor)', () => {
|
|
239
|
+
it('registers as write-access COOKIE browser adapter with username/url/result columns', () => {
|
|
240
|
+
expect(followCommand.access).toBe('write');
|
|
241
|
+
expect(followCommand.browser).toBe(true);
|
|
242
|
+
expect(followCommand.strategy).toBe('cookie');
|
|
243
|
+
expect(followCommand.columns).toEqual(['username', 'url', 'result']);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('validates username upfront before navigating', async () => {
|
|
247
|
+
const page = makePage([sampleFollowRow]);
|
|
248
|
+
await expect(followCommand.func(page, { username: '' })).rejects.toBeInstanceOf(ArgumentError);
|
|
249
|
+
await expect(followCommand.func(page, { username: 'bad name' })).rejects.toBeInstanceOf(ArgumentError);
|
|
250
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('navigates to /@username (with @ stripped) and returns the row from evaluate', async () => {
|
|
254
|
+
const page = makePage([sampleFollowRow]);
|
|
255
|
+
const rows = await followCommand.func(page, { username: '@creator' });
|
|
256
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.tiktok.com/@creator', { waitUntil: 'load', settleMs: 5000 });
|
|
257
|
+
expect(rows).toEqual([sampleFollowRow]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('maps AUTH_REQUIRED to AuthRequiredError; other failures get retryable=true hint', async () => {
|
|
261
|
+
await expect(followCommand.func(
|
|
262
|
+
makeFailingPage(new Error('AUTH_REQUIRED: TikTok login required')),
|
|
263
|
+
{ username: 'creator' },
|
|
264
|
+
)).rejects.toBeInstanceOf(AuthRequiredError);
|
|
265
|
+
try {
|
|
266
|
+
await followCommand.func(
|
|
267
|
+
makeFailingPage(new Error('STATE_VERIFY_FAIL: follow button did not flip')),
|
|
268
|
+
{ username: 'creator' },
|
|
269
|
+
);
|
|
270
|
+
throw new Error('should have thrown');
|
|
271
|
+
} catch (err) {
|
|
272
|
+
expect(err).toBeInstanceOf(CommandExecutionError);
|
|
273
|
+
expect(err.message).toMatch(/Failed to follow @creator/);
|
|
274
|
+
expect(err.hint).toMatch(/retryable=true/);
|
|
275
|
+
expect(err.hint).toMatch(/idempotent/);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('maps navigation failures and empty evaluate rows to retryable CommandExecutionError', async () => {
|
|
280
|
+
await expect(followCommand.func(
|
|
281
|
+
makeGotoFailingPage(new Error('Execution context was destroyed')),
|
|
282
|
+
{ username: 'creator' },
|
|
283
|
+
)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
284
|
+
await expect(followCommand.func(
|
|
285
|
+
makePage([]),
|
|
286
|
+
{ username: 'creator' },
|
|
287
|
+
)).rejects.toMatchObject({ hint: expect.stringMatching(/retryable=true/) });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('build script embeds username via JSON.stringify and pins all relation labels', () => {
|
|
291
|
+
const script = followTest.buildFollowScript('creator');
|
|
292
|
+
expect(script).toContain('const username = "creator";');
|
|
293
|
+
expect(script).toContain('ensureLoggedInOrThrow()');
|
|
294
|
+
expect(script).toContain('ensureNoRateLimitOrThrow()');
|
|
295
|
+
expect(script).toContain("'Follow', '关注'");
|
|
296
|
+
expect(script).toContain("'Following', '已关注'");
|
|
297
|
+
expect(script).toContain("'Friends', '互关'");
|
|
298
|
+
expect(script).toContain("'already-following'");
|
|
299
|
+
expect(script).toContain("'already-friends'");
|
|
300
|
+
expect(script).toContain("result: 'followed'");
|
|
301
|
+
expect(script).not.toContain('becameFriends ?');
|
|
302
|
+
expect(script).toContain('STATE_VERIFY_FAIL');
|
|
303
|
+
expect(script).toContain('BUTTON_NOT_FOUND');
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('tiktok/unfollow (Route 1 button-walker refactor)', () => {
|
|
308
|
+
it('registers as write-access COOKIE browser adapter with username/url/result columns', () => {
|
|
309
|
+
expect(unfollowCommand.access).toBe('write');
|
|
310
|
+
expect(unfollowCommand.browser).toBe(true);
|
|
311
|
+
expect(unfollowCommand.strategy).toBe('cookie');
|
|
312
|
+
expect(unfollowCommand.columns).toEqual(['username', 'url', 'result']);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('validates username upfront before navigating', async () => {
|
|
316
|
+
const page = makePage([sampleUnfollowRow]);
|
|
317
|
+
await expect(unfollowCommand.func(page, { username: '' })).rejects.toBeInstanceOf(ArgumentError);
|
|
318
|
+
await expect(unfollowCommand.func(page, { username: 'bad/name' })).rejects.toBeInstanceOf(ArgumentError);
|
|
319
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('navigates to /@username and returns the row from evaluate', async () => {
|
|
323
|
+
const page = makePage([sampleUnfollowRow]);
|
|
324
|
+
const rows = await unfollowCommand.func(page, { username: 'creator' });
|
|
325
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.tiktok.com/@creator', { waitUntil: 'load', settleMs: 5000 });
|
|
326
|
+
expect(rows).toEqual([sampleUnfollowRow]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('maps AUTH_REQUIRED to AuthRequiredError; other failures get retryable=true hint', async () => {
|
|
330
|
+
await expect(unfollowCommand.func(
|
|
331
|
+
makeFailingPage(new Error('AUTH_REQUIRED: login required')),
|
|
332
|
+
{ username: 'creator' },
|
|
333
|
+
)).rejects.toBeInstanceOf(AuthRequiredError);
|
|
334
|
+
try {
|
|
335
|
+
await unfollowCommand.func(
|
|
336
|
+
makeFailingPage(new Error('STATE_VERIFY_FAIL: relation did not flip back')),
|
|
337
|
+
{ username: 'creator' },
|
|
338
|
+
);
|
|
339
|
+
throw new Error('should have thrown');
|
|
340
|
+
} catch (err) {
|
|
341
|
+
expect(err).toBeInstanceOf(CommandExecutionError);
|
|
342
|
+
expect(err.message).toMatch(/Failed to unfollow @creator/);
|
|
343
|
+
expect(err.hint).toMatch(/retryable=true/);
|
|
344
|
+
expect(err.hint).toMatch(/idempotent/);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('maps navigation failures and empty evaluate rows to retryable CommandExecutionError', async () => {
|
|
349
|
+
await expect(unfollowCommand.func(
|
|
350
|
+
makeGotoFailingPage(new Error('Execution context was destroyed')),
|
|
351
|
+
{ username: 'creator' },
|
|
352
|
+
)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
353
|
+
await expect(unfollowCommand.func(
|
|
354
|
+
makePage([]),
|
|
355
|
+
{ username: 'creator' },
|
|
356
|
+
)).rejects.toMatchObject({ hint: expect.stringMatching(/retryable=true/) });
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('build script handles confirm-dialog flow + flips back to Follow', () => {
|
|
360
|
+
const script = unfollowTest.buildUnfollowScript('creator');
|
|
361
|
+
expect(script).toContain('const username = "creator";');
|
|
362
|
+
expect(script).toContain('ensureLoggedInOrThrow()');
|
|
363
|
+
expect(script).toContain('ensureNoRateLimitOrThrow()');
|
|
364
|
+
expect(script).toContain("'Unfollow', '取消关注'");
|
|
365
|
+
expect(script).toContain("'already-not-following'");
|
|
366
|
+
expect(script).toContain("'unfollowed'");
|
|
367
|
+
expect(script).toContain('STATE_VERIFY_FAIL');
|
|
368
|
+
expect(script).toContain('BUTTON_NOT_FOUND');
|
|
369
|
+
});
|
|
370
|
+
});
|
package/clis/toutiao/articles.js
CHANGED
|
@@ -1,53 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
]);
|
|
9
|
-
const lines = String(text || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
10
|
-
const results = [];
|
|
11
|
-
|
|
12
|
-
for (let i = 0; i < lines.length; i++) {
|
|
13
|
-
const line = lines[i];
|
|
14
|
-
if (!/^\d{2}-\d{2}\s+\d{2}:\d{2}$/.test(line)) continue;
|
|
15
|
-
|
|
16
|
-
const date = line;
|
|
17
|
-
let title = '';
|
|
18
|
-
let status = '';
|
|
19
|
-
let stats = null;
|
|
20
|
-
|
|
21
|
-
for (let back = 3; back >= 1; back--) {
|
|
22
|
-
const prev = lines[i - back] || '';
|
|
23
|
-
if (!prev || prev.length >= 100 || /^\d+$/.test(prev) || NON_TITLE_LINES.has(prev)) continue;
|
|
24
|
-
title = prev;
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
for (let fwd = 1; fwd < 8; fwd++) {
|
|
29
|
-
const fwdLine = lines[i + fwd] || '';
|
|
30
|
-
if (fwdLine === '已发布' || fwdLine === '定时发布中' || fwdLine === '审核中' || fwdLine === '由文章生成') {
|
|
31
|
-
status = fwdLine;
|
|
32
|
-
}
|
|
33
|
-
if (fwdLine.includes('展现') && fwdLine.includes('阅读')) {
|
|
34
|
-
const match = fwdLine.match(/展现\s*([\d,]+)\s*阅读\s*([\d,]+)\s*点赞\s*([\d,]+)\s*评论\s*([\d,]*)/);
|
|
35
|
-
if (match) {
|
|
36
|
-
stats = {
|
|
37
|
-
'展现': match[1],
|
|
38
|
-
'阅读': match[2],
|
|
39
|
-
'点赞': match[3],
|
|
40
|
-
'评论': match[4] || '0',
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (title && stats) results.push({ title, date, status, ...stats });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return results;
|
|
50
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Toutiao creator-backend article list — extracts article rows + basic metrics
|
|
3
|
+
* from the rendered creator dashboard page text.
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
6
|
+
import { AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
7
|
+
import { looksToutiaoAuthWallText, parseArticlesPage, parseToutiaoArticlesText } from './utils.js';
|
|
51
8
|
|
|
52
9
|
cli({
|
|
53
10
|
site: 'toutiao',
|
|
@@ -55,27 +12,44 @@ cli({
|
|
|
55
12
|
access: 'read',
|
|
56
13
|
description: '获取头条号创作者后台文章列表及数据',
|
|
57
14
|
domain: 'mp.toutiao.com',
|
|
15
|
+
strategy: Strategy.COOKIE,
|
|
16
|
+
browser: true,
|
|
58
17
|
args: [
|
|
59
18
|
{ name: 'page', type: 'int', default: 1, help: '页码 (1-4)' },
|
|
60
19
|
],
|
|
61
20
|
columns: ['title', 'date', 'status', '展现', '阅读', '点赞', '评论'],
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
const articlePage = parseArticlesPage(kwargs.page, 1);
|
|
23
|
+
let text;
|
|
24
|
+
try {
|
|
25
|
+
await page.goto(`https://mp.toutiao.com/profile_v4/manage/content/all?page=${articlePage}`);
|
|
26
|
+
await page.wait('networkidle');
|
|
27
|
+
await page.wait(3);
|
|
28
|
+
text = await page.evaluate(`
|
|
68
29
|
(async () => {
|
|
69
|
-
// Wait for content to load
|
|
70
30
|
await new Promise(r => setTimeout(r, 2000));
|
|
71
|
-
|
|
72
|
-
return parse(document.body.innerText || '');
|
|
31
|
+
return document.body.innerText || '';
|
|
73
32
|
})()
|
|
74
|
-
`
|
|
75
|
-
}
|
|
76
|
-
|
|
33
|
+
`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new CommandExecutionError(`toutiao articles render failed: ${error?.message || error}`);
|
|
36
|
+
}
|
|
37
|
+
if (looksToutiaoAuthWallText(text)) {
|
|
38
|
+
throw new AuthRequiredError('mp.toutiao.com', 'Toutiao creator articles require a logged-in mp.toutiao.com browser session');
|
|
39
|
+
}
|
|
40
|
+
const rows = parseToutiaoArticlesText(text);
|
|
41
|
+
if (rows.length === 0) {
|
|
42
|
+
throw new EmptyResultError(
|
|
43
|
+
'toutiao articles',
|
|
44
|
+
`未抓取到创作者后台文章 (page=${articlePage})。可能页面尚未完成渲染或无文章。`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
},
|
|
77
49
|
});
|
|
78
50
|
|
|
51
|
+
export { parseToutiaoArticlesText };
|
|
79
52
|
export const __test__ = {
|
|
80
53
|
parseToutiaoArticlesText,
|
|
54
|
+
parseArticlesPage,
|
|
81
55
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toutiao homepage hot-board — public trending news topics on Toutiao.
|
|
3
|
+
*
|
|
4
|
+
* Backed by the public hot-event/hot-board endpoint which serves the same JSON
|
|
5
|
+
* the toutiao.com homepage hot panel renders. No authentication required.
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
8
|
+
import {
|
|
9
|
+
CommandExecutionError,
|
|
10
|
+
EmptyResultError,
|
|
11
|
+
} from '@jackwener/opencli/errors';
|
|
12
|
+
import { HOT_BOARD_URL, mapHotRow, parseHotLimit } from './utils.js';
|
|
13
|
+
|
|
14
|
+
cli({
|
|
15
|
+
site: 'toutiao',
|
|
16
|
+
name: 'hot',
|
|
17
|
+
access: 'read',
|
|
18
|
+
description: '今日头条首页热榜(公开 API,无需登录)',
|
|
19
|
+
domain: 'www.toutiao.com',
|
|
20
|
+
strategy: Strategy.PUBLIC,
|
|
21
|
+
browser: false,
|
|
22
|
+
args: [
|
|
23
|
+
{ name: 'limit', type: 'int', default: 30, help: '返回条数 (1-50)' },
|
|
24
|
+
],
|
|
25
|
+
columns: ['rank', 'group_id', 'title', 'query', 'hot_value', 'label', 'url', 'image_url'],
|
|
26
|
+
func: async (_page, kwargs) => {
|
|
27
|
+
const limit = parseHotLimit(kwargs?.limit, 30);
|
|
28
|
+
let resp;
|
|
29
|
+
try {
|
|
30
|
+
resp = await fetch(HOT_BOARD_URL, {
|
|
31
|
+
headers: {
|
|
32
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
33
|
+
Accept: 'application/json',
|
|
34
|
+
Referer: 'https://www.toutiao.com/',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new CommandExecutionError(`toutiao hot-board request failed: ${error?.message || error}`);
|
|
39
|
+
}
|
|
40
|
+
if (!resp.ok) {
|
|
41
|
+
throw new CommandExecutionError(`toutiao hot-board failed: HTTP ${resp.status}`);
|
|
42
|
+
}
|
|
43
|
+
let payload;
|
|
44
|
+
try {
|
|
45
|
+
payload = await resp.json();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new CommandExecutionError(`toutiao hot-board returned malformed JSON: ${error?.message || error}`);
|
|
48
|
+
}
|
|
49
|
+
if (payload?.status && payload.status !== 'success') {
|
|
50
|
+
throw new CommandExecutionError(`toutiao hot-board returned status=${payload.status}`);
|
|
51
|
+
}
|
|
52
|
+
if (payload?.error || payload?.message) {
|
|
53
|
+
throw new CommandExecutionError(`toutiao hot-board returned error: ${payload.error || payload.message}`);
|
|
54
|
+
}
|
|
55
|
+
const list = Array.isArray(payload?.data) ? payload.data : [];
|
|
56
|
+
const rows = list.map(mapHotRow).filter(Boolean).slice(0, limit);
|
|
57
|
+
if (rows.length === 0) {
|
|
58
|
+
throw new EmptyResultError('toutiao hot', '上游 hot-board 返回空列表。');
|
|
59
|
+
}
|
|
60
|
+
// Re-rank (1..N) after filter so ranks are dense even if upstream had nulls.
|
|
61
|
+
return rows.map((row, idx) => ({ ...row, rank: idx + 1 }));
|
|
62
|
+
},
|
|
63
|
+
});
|