@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,99 @@
|
|
|
1
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
import { parseTweetUrl } from './shared.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'twitter',
|
|
7
|
+
name: 'unretweet',
|
|
8
|
+
access: 'write',
|
|
9
|
+
description: 'Undo a retweet on a specific tweet',
|
|
10
|
+
domain: 'x.com',
|
|
11
|
+
strategy: Strategy.UI,
|
|
12
|
+
browser: true,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'url', type: 'string', required: true, positional: true, help: 'The URL of the tweet to unretweet' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['status', 'message'],
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
if (!page)
|
|
19
|
+
throw new CommandExecutionError('Browser session required for twitter unretweet');
|
|
20
|
+
const target = parseTweetUrl(kwargs.url);
|
|
21
|
+
await page.goto(target.url);
|
|
22
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
23
|
+
const result = await page.evaluate(`(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const tweetId = ${JSON.stringify(target.id)};
|
|
26
|
+
const findTargetArticle = () => Array.from(document.querySelectorAll('article')).find((article) =>
|
|
27
|
+
Array.from(article.querySelectorAll('a[href*="/status/"]')).some((link) => {
|
|
28
|
+
try {
|
|
29
|
+
const match = new URL(link.href, window.location.origin).pathname.match(/^\/(?:[^/]+|i)\/status\/(\d+)\/?$/);
|
|
30
|
+
return match?.[1] === tweetId;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
// Poll for the tweet to render
|
|
37
|
+
let attempts = 0;
|
|
38
|
+
let retweetBtn = null;
|
|
39
|
+
let unretweetBtn = null;
|
|
40
|
+
let targetArticle = null;
|
|
41
|
+
|
|
42
|
+
while (attempts < 20) {
|
|
43
|
+
targetArticle = findTargetArticle();
|
|
44
|
+
retweetBtn = targetArticle?.querySelector('[data-testid="retweet"]') || null;
|
|
45
|
+
unretweetBtn = targetArticle?.querySelector('[data-testid="unretweet"]') || null;
|
|
46
|
+
|
|
47
|
+
if (retweetBtn || unretweetBtn) break;
|
|
48
|
+
|
|
49
|
+
await new Promise(r => setTimeout(r, 500));
|
|
50
|
+
attempts++;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Already not retweeted: idempotent success
|
|
54
|
+
if (retweetBtn) {
|
|
55
|
+
return { ok: true, message: 'Tweet is not retweeted (already removed).' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!unretweetBtn) {
|
|
59
|
+
return { ok: false, message: 'Could not find the Unretweet button on this tweet after waiting 10 seconds. Are you logged in?' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Step 1: click Unretweet button → opens menu
|
|
63
|
+
unretweetBtn.click();
|
|
64
|
+
|
|
65
|
+
// Step 2: wait for the confirm menu item to appear, then click it
|
|
66
|
+
let confirmBtn = null;
|
|
67
|
+
for (let i = 0; i < 20; i++) {
|
|
68
|
+
await new Promise(r => setTimeout(r, 250));
|
|
69
|
+
confirmBtn = document.querySelector('[data-testid="unretweetConfirm"]');
|
|
70
|
+
if (confirmBtn) break;
|
|
71
|
+
}
|
|
72
|
+
if (!confirmBtn) {
|
|
73
|
+
return { ok: false, message: 'Unretweet menu opened but the confirm option did not appear.' };
|
|
74
|
+
}
|
|
75
|
+
confirmBtn.click();
|
|
76
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
77
|
+
|
|
78
|
+
// Verify success by checking if the 'retweet' button reappeared
|
|
79
|
+
const verifyArticle = findTargetArticle() || targetArticle;
|
|
80
|
+
const verifyBtn = verifyArticle?.querySelector('[data-testid="retweet"]');
|
|
81
|
+
if (verifyBtn) {
|
|
82
|
+
return { ok: true, message: 'Tweet successfully unretweeted.' };
|
|
83
|
+
} else {
|
|
84
|
+
return { ok: false, message: 'Unretweet action was initiated but UI did not update as expected.' };
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return { ok: false, message: e.toString() };
|
|
88
|
+
}
|
|
89
|
+
})()`);
|
|
90
|
+
if (result.ok) {
|
|
91
|
+
// Wait for the unretweet network request to be processed
|
|
92
|
+
await page.wait(2);
|
|
93
|
+
}
|
|
94
|
+
return [{
|
|
95
|
+
status: result.ok ? 'success' : 'failed',
|
|
96
|
+
message: result.message
|
|
97
|
+
}];
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import './unretweet.js';
|
|
5
|
+
import { createPageMock } from '../test-utils.js';
|
|
6
|
+
|
|
7
|
+
describe('twitter unretweet command', () => {
|
|
8
|
+
it('clicks the unretweet button then the confirm menu item and reports success', async () => {
|
|
9
|
+
const cmd = getRegistry().get('twitter/unretweet');
|
|
10
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
11
|
+
const page = createPageMock([
|
|
12
|
+
{ ok: true, message: 'Tweet successfully unretweeted.' },
|
|
13
|
+
]);
|
|
14
|
+
const result = await cmd.func(page, {
|
|
15
|
+
url: 'https://x.com/alice/status/2040254679301718161',
|
|
16
|
+
});
|
|
17
|
+
expect(page.goto).toHaveBeenCalledWith('https://x.com/alice/status/2040254679301718161');
|
|
18
|
+
expect(page.wait).toHaveBeenNthCalledWith(1, { selector: '[data-testid="primaryColumn"]' });
|
|
19
|
+
expect(page.wait).toHaveBeenNthCalledWith(2, 2);
|
|
20
|
+
const script = page.evaluate.mock.calls[0][0];
|
|
21
|
+
// Two-step UI flow must be present:
|
|
22
|
+
// 1) click the unretweet button
|
|
23
|
+
// 2) wait for and click the confirm menu item (data-testid="unretweetConfirm")
|
|
24
|
+
expect(script).toContain('unretweetBtn.click()');
|
|
25
|
+
expect(script).toContain("document.querySelector('[data-testid=\"unretweetConfirm\"]')");
|
|
26
|
+
expect(script).toContain('confirmBtn.click()');
|
|
27
|
+
expect(script).toContain("document.querySelectorAll('article')");
|
|
28
|
+
expect(script).toContain('match?.[1] === tweetId');
|
|
29
|
+
expect(script).toContain("targetArticle?.querySelector('[data-testid=\"unretweet\"]')");
|
|
30
|
+
// Idempotency probe: when already not retweeted ([data-testid="retweet"] present),
|
|
31
|
+
// the script returns ok:true with an "already removed" message.
|
|
32
|
+
expect(script).toContain("targetArticle?.querySelector('[data-testid=\"retweet\"]')");
|
|
33
|
+
expect(result).toEqual([
|
|
34
|
+
{ status: 'success', message: 'Tweet successfully unretweeted.' },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns a failed row when the confirm menu item never appears', async () => {
|
|
39
|
+
const cmd = getRegistry().get('twitter/unretweet');
|
|
40
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
41
|
+
const page = createPageMock([
|
|
42
|
+
{ ok: false, message: 'Unretweet menu opened but the confirm option did not appear.' },
|
|
43
|
+
]);
|
|
44
|
+
const result = await cmd.func(page, {
|
|
45
|
+
url: 'https://x.com/alice/status/2040254679301718161',
|
|
46
|
+
});
|
|
47
|
+
expect(result).toEqual([
|
|
48
|
+
{ status: 'failed', message: 'Unretweet menu opened but the confirm option did not appear.' },
|
|
49
|
+
]);
|
|
50
|
+
expect(page.wait).toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('throws CommandExecutionError when no page is provided', async () => {
|
|
54
|
+
const cmd = getRegistry().get('twitter/unretweet');
|
|
55
|
+
await expect(cmd.func(undefined, {
|
|
56
|
+
url: 'https://x.com/alice/status/2040254679301718161',
|
|
57
|
+
})).rejects.toThrow(CommandExecutionError);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('rejects invalid tweet URLs before navigation', async () => {
|
|
61
|
+
const cmd = getRegistry().get('twitter/unretweet');
|
|
62
|
+
const page = createPageMock([]);
|
|
63
|
+
await expect(cmd.func(page, {
|
|
64
|
+
url: 'http://x.com/alice/status/2040254679301718161',
|
|
65
|
+
})).rejects.toThrow(ArgumentError);
|
|
66
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
67
|
+
expect(page.evaluate).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError, getErrorMessage } from '@jackwener/opencli/errors';
|
|
3
|
+
|
|
4
|
+
const UISDC_NEWS_URL = 'https://www.uisdc.com/news';
|
|
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 uisdc news --limit ${DEFAULT_LIMIT}`);
|
|
13
|
+
}
|
|
14
|
+
if (limit > MAX_LIMIT) {
|
|
15
|
+
throw new ArgumentError(`limit must be <= ${MAX_LIMIT}`, `Example: opencli uisdc 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 buildExtractUisdcNewsJs() {
|
|
25
|
+
return `
|
|
26
|
+
(() => {
|
|
27
|
+
const cards = Array.from(document.querySelectorAll(
|
|
28
|
+
'.news-list > .news-item:first-child > .item-content > .dubao-items > .dubao-item'
|
|
29
|
+
));
|
|
30
|
+
if (cards.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
reason: 'selector-missing',
|
|
34
|
+
title: document.title || '',
|
|
35
|
+
bodyText: (document.body?.innerText || document.body?.textContent || '').slice(0, 500),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const rows = cards.map((el, index) => {
|
|
39
|
+
const anchor = el.querySelector('a[href]');
|
|
40
|
+
return {
|
|
41
|
+
rank: index + 1,
|
|
42
|
+
title: el.querySelector('.dubao-title')?.textContent || '',
|
|
43
|
+
summary: el.querySelector('.dubao-content')?.textContent || '',
|
|
44
|
+
url: anchor ? new URL(anchor.getAttribute('href'), location.href).href : '',
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
return { ok: true, rows };
|
|
48
|
+
})()
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toRows(payload, limit) {
|
|
53
|
+
if (!payload || typeof payload !== 'object') {
|
|
54
|
+
throw new CommandExecutionError('UISDC news page returned an unreadable payload');
|
|
55
|
+
}
|
|
56
|
+
if (!payload.ok) {
|
|
57
|
+
const reason = typeof payload.reason === 'string' && payload.reason.trim() ? payload.reason.trim() : 'selector-drift';
|
|
58
|
+
throw new CommandExecutionError(
|
|
59
|
+
`UISDC news selector drift: ${reason}`,
|
|
60
|
+
payload.title ? `Page title: ${payload.title}` : undefined,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const rows = (Array.isArray(payload.rows) ? payload.rows : [])
|
|
64
|
+
.map((row, index) => ({
|
|
65
|
+
rank: index + 1,
|
|
66
|
+
title: normalizeText(row.title),
|
|
67
|
+
summary: normalizeText(row.summary),
|
|
68
|
+
url: normalizeText(row.url),
|
|
69
|
+
}))
|
|
70
|
+
.filter((row) => row.title && row.url);
|
|
71
|
+
if (rows.length === 0) {
|
|
72
|
+
throw new EmptyResultError('uisdc news', 'UISDC news page loaded, but no news rows with title and URL were extracted.');
|
|
73
|
+
}
|
|
74
|
+
return rows.slice(0, limit).map((row, index) => ({ ...row, rank: index + 1 }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function loadUisdcNews(page, args) {
|
|
78
|
+
const limit = normalizeLimit(args.limit);
|
|
79
|
+
await page.goto(UISDC_NEWS_URL, { waitUntil: 'load', settleMs: 3000 });
|
|
80
|
+
const payload = await page.evaluate(buildExtractUisdcNewsJs()).catch((error) => {
|
|
81
|
+
throw new CommandExecutionError(`Failed to extract UISDC news: ${getErrorMessage(error)}`);
|
|
82
|
+
});
|
|
83
|
+
return toRows(payload, limit);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const uisdcNewsCommand = cli({
|
|
87
|
+
site: 'uisdc',
|
|
88
|
+
name: 'news',
|
|
89
|
+
access: 'read',
|
|
90
|
+
description: '优设读报 - 最新 AI/设计行业新闻',
|
|
91
|
+
domain: 'www.uisdc.com',
|
|
92
|
+
strategy: Strategy.PUBLIC,
|
|
93
|
+
browser: true,
|
|
94
|
+
args: [
|
|
95
|
+
{ name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of news items to return (max ${MAX_LIMIT})` },
|
|
96
|
+
],
|
|
97
|
+
columns: ['rank', 'title', 'summary', 'url'],
|
|
98
|
+
func: loadUisdcNews,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const __test__ = {
|
|
102
|
+
buildExtractUisdcNewsJs,
|
|
103
|
+
normalizeLimit,
|
|
104
|
+
toRows,
|
|
105
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
import { uisdcNewsCommand, __test__ } from './news.js';
|
|
5
|
+
|
|
6
|
+
function runBrowserScript(html, script, url = 'https://www.uisdc.com/news') {
|
|
7
|
+
const dom = new JSDOM(html, { url, runScripts: 'outside-only' });
|
|
8
|
+
return dom.window.eval(script);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makePage(evaluateResult) {
|
|
12
|
+
return {
|
|
13
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('uisdc/news', () => {
|
|
19
|
+
it('registers stable URL in columns', () => {
|
|
20
|
+
expect(uisdcNewsCommand.access).toBe('read');
|
|
21
|
+
expect(uisdcNewsCommand.columns).toEqual(['rank', 'title', 'summary', 'url']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('validates limit before browser navigation', async () => {
|
|
25
|
+
const page = makePage({ ok: true, rows: [] });
|
|
26
|
+
await expect(uisdcNewsCommand.func(page, { limit: 0 })).rejects.toBeInstanceOf(ArgumentError);
|
|
27
|
+
await expect(uisdcNewsCommand.func(page, { limit: 51 })).rejects.toBeInstanceOf(ArgumentError);
|
|
28
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('extracts rows from the UISDC news DOM', async () => {
|
|
32
|
+
const html = `
|
|
33
|
+
<div class="news-list">
|
|
34
|
+
<div class="news-item">
|
|
35
|
+
<div class="item-content">
|
|
36
|
+
<div class="dubao-items">
|
|
37
|
+
<div class="dubao-item">
|
|
38
|
+
<a href="/article-1"><span class="dubao-title"> AI design news </span></a>
|
|
39
|
+
<div class="dubao-content"> summary text </div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
const payload = runBrowserScript(html, __test__.buildExtractUisdcNewsJs());
|
|
47
|
+
const page = makePage(payload);
|
|
48
|
+
|
|
49
|
+
const rows = await uisdcNewsCommand.func(page, { limit: 10 });
|
|
50
|
+
|
|
51
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.uisdc.com/news', { waitUntil: 'load', settleMs: 3000 });
|
|
52
|
+
expect(rows).toEqual([{
|
|
53
|
+
rank: 1,
|
|
54
|
+
title: 'AI design news',
|
|
55
|
+
summary: 'summary text',
|
|
56
|
+
url: 'https://www.uisdc.com/article-1',
|
|
57
|
+
}]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('maps selector drift and empty rows to typed errors', async () => {
|
|
61
|
+
await expect(uisdcNewsCommand.func(makePage({ ok: false, reason: 'selector-missing' }), { limit: 1 }))
|
|
62
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
63
|
+
await expect(uisdcNewsCommand.func(makePage({ ok: true, rows: [{ title: '', url: '' }] }), { limit: 1 }))
|
|
64
|
+
.rejects.toBeInstanceOf(EmptyResultError);
|
|
65
|
+
});
|
|
66
|
+
});
|
package/clis/wanfang/search.js
CHANGED
|
@@ -14,7 +14,6 @@ cli({
|
|
|
14
14
|
{ name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
|
|
15
15
|
],
|
|
16
16
|
columns: ['rank', 'title', 'authors', 'source', 'year', 'type', 'cited', 'url'],
|
|
17
|
-
navigateBefore: false,
|
|
18
17
|
func: async (page, kwargs) => {
|
|
19
18
|
const limit = clampInt(kwargs.limit, 10, 1, 20);
|
|
20
19
|
const query = requireNonEmptyQuery(kwargs.query);
|
package/clis/web/read.js
CHANGED
|
@@ -18,6 +18,7 @@ import { downloadArticle } from '@jackwener/opencli/download/article-download';
|
|
|
18
18
|
|
|
19
19
|
const NETWORK_IDLE_QUIET_MS = 1000;
|
|
20
20
|
const NETWORK_IDLE_POLL_MS = 500;
|
|
21
|
+
const MIN_NON_STRUCTURAL_IFRAME_TEXT_CHARS = 50;
|
|
21
22
|
|
|
22
23
|
function sleep(ms) {
|
|
23
24
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -31,7 +32,7 @@ function boolish(value) {
|
|
|
31
32
|
|
|
32
33
|
function normalizeFrameMode(value) {
|
|
33
34
|
const mode = String(value || 'same-origin').toLowerCase();
|
|
34
|
-
if (['same-origin', 'none'].includes(mode)) return mode;
|
|
35
|
+
if (['same-origin', 'all-same-origin', 'none'].includes(mode)) return mode;
|
|
35
36
|
return 'same-origin';
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -144,6 +145,7 @@ function buildRenderAwareExtractorJs(options) {
|
|
|
144
145
|
return `
|
|
145
146
|
(() => {
|
|
146
147
|
const frameMode = ${JSON.stringify(options.frames)};
|
|
148
|
+
const minNonStructuralIframeTextChars = ${MIN_NON_STRUCTURAL_IFRAME_TEXT_CHARS};
|
|
147
149
|
const result = {
|
|
148
150
|
title: '',
|
|
149
151
|
author: '',
|
|
@@ -188,6 +190,7 @@ function buildRenderAwareExtractorJs(options) {
|
|
|
188
190
|
const collectEmptyContainers = (root, scope, baseUrl) => {
|
|
189
191
|
const likely = 'table, tbody, ul[id], ol[id], div[id], section[id], [class*="grid"], [class*="data"], [class*="list"], [id*="grid"], [id*="data"], [id*="list"]';
|
|
190
192
|
root.querySelectorAll?.(likely).forEach((el) => {
|
|
193
|
+
if (scope === 'main' && el.closest?.('[data-opencli-iframe-source]')) return;
|
|
191
194
|
const id = el.getAttribute('id') || '';
|
|
192
195
|
const cls = el.getAttribute('class') || '';
|
|
193
196
|
const name = [id, cls].join(' ').toLowerCase();
|
|
@@ -202,6 +205,29 @@ function buildRenderAwareExtractorJs(options) {
|
|
|
202
205
|
});
|
|
203
206
|
});
|
|
204
207
|
};
|
|
208
|
+
const hasDataContainerSignal = (root) => {
|
|
209
|
+
const likely = 'table, tbody, ul[id], ol[id], [id*="grid"], [id*="data"], [id*="list"], [id*="content"], [id*="result"], [class*="grid"], [class*="data"], [class*="list"], [class*="content"], [class*="result"]';
|
|
210
|
+
return !!root.querySelector?.(likely);
|
|
211
|
+
};
|
|
212
|
+
const shouldIncludeExternalFrame = (frameBody) => {
|
|
213
|
+
// Outside-content iframes are less trusted than placeholders inside
|
|
214
|
+
// contentEl. Long plain text is the fallback for simple same-origin
|
|
215
|
+
// frames that lack article/table/list structure.
|
|
216
|
+
if (textLen(frameBody) >= minNonStructuralIframeTextChars) return true;
|
|
217
|
+
if (frameBody.querySelector?.('article, main, [role="main"], table, tbody, ul li, ol li')) return true;
|
|
218
|
+
return hasDataContainerSignal(frameBody);
|
|
219
|
+
};
|
|
220
|
+
const buildFrameSection = (frameBody, desc, fallbackLabel) => {
|
|
221
|
+
absolutizeTree(frameBody, desc.src || window.location.href);
|
|
222
|
+
collectEmptyContainers(frameBody, 'iframe', desc.src);
|
|
223
|
+
const section = document.createElement('section');
|
|
224
|
+
section.setAttribute('data-opencli-iframe-source', desc.src);
|
|
225
|
+
const heading = document.createElement('h2');
|
|
226
|
+
heading.textContent = '来自 iframe: ' + (desc.src || fallbackLabel);
|
|
227
|
+
section.appendChild(heading);
|
|
228
|
+
Array.from(frameBody.childNodes).forEach(node => section.appendChild(node));
|
|
229
|
+
return section;
|
|
230
|
+
};
|
|
205
231
|
|
|
206
232
|
const ogTitle = document.querySelector('meta[property="og:title"]');
|
|
207
233
|
if (ogTitle) result.title = ogTitle.getAttribute('content')?.trim() || '';
|
|
@@ -252,28 +278,32 @@ function buildRenderAwareExtractorJs(options) {
|
|
|
252
278
|
|
|
253
279
|
const originalFrames = Array.from(contentEl.querySelectorAll('iframe'));
|
|
254
280
|
const clonedFrames = Array.from(clone.querySelectorAll('iframe'));
|
|
281
|
+
const clonedFrameByOriginal = new Map();
|
|
282
|
+
originalFrames.forEach((frame, index) => {
|
|
283
|
+
const cloned = clonedFrames[index];
|
|
284
|
+
if (cloned) clonedFrameByOriginal.set(frame, cloned);
|
|
285
|
+
});
|
|
255
286
|
const allFrames = Array.from(document.querySelectorAll('iframe'));
|
|
256
|
-
|
|
287
|
+
const frameDescriptions = new Map();
|
|
288
|
+
allFrames.forEach((frame, index) => frameDescriptions.set(frame, describeFrame(frame, index)));
|
|
289
|
+
const getFrameDescription = (frame, fallbackIndex) => frameDescriptions.get(frame) || describeFrame(frame, fallbackIndex);
|
|
290
|
+
result.diagnostics.frames = allFrames.map(frame => frameDescriptions.get(frame));
|
|
257
291
|
|
|
258
|
-
if (frameMode === 'same-origin') {
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
292
|
+
if (frameMode === 'same-origin' || frameMode === 'all-same-origin') {
|
|
293
|
+
allFrames.forEach((frame, index) => {
|
|
294
|
+
const insideContent = contentEl.contains(frame);
|
|
295
|
+
const cloned = insideContent ? clonedFrameByOriginal.get(frame) : null;
|
|
296
|
+
if (insideContent && !cloned) return;
|
|
297
|
+
const desc = getFrameDescription(frame, index);
|
|
263
298
|
if (!desc.sameOrigin || !desc.accessible) return;
|
|
264
299
|
try {
|
|
265
300
|
const doc = frame.contentDocument;
|
|
266
301
|
if (!doc?.body) return;
|
|
267
302
|
const frameBody = doc.body.cloneNode(true);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const heading = document.createElement('h2');
|
|
273
|
-
heading.textContent = '来自 iframe: ' + (desc.src || frame.getAttribute('src') || ('#' + index));
|
|
274
|
-
section.appendChild(heading);
|
|
275
|
-
Array.from(frameBody.childNodes).forEach(node => section.appendChild(node));
|
|
276
|
-
cloned.replaceWith(section);
|
|
303
|
+
if (frameMode !== 'all-same-origin' && !insideContent && !shouldIncludeExternalFrame(frameBody)) return;
|
|
304
|
+
const section = buildFrameSection(frameBody, desc, frame.getAttribute('src') || ('#' + index));
|
|
305
|
+
if (insideContent) cloned.replaceWith(section);
|
|
306
|
+
else clone.appendChild(section);
|
|
277
307
|
result.diagnostics.includedFrameCount += 1;
|
|
278
308
|
} catch {}
|
|
279
309
|
});
|
|
@@ -376,7 +406,7 @@ const command = cli({
|
|
|
376
406
|
{ name: 'wait', type: 'int', default: 3, help: 'Seconds to wait after page load' },
|
|
377
407
|
{ name: 'wait-for', valueRequired: true, help: 'CSS selector to wait for in the main document or same-origin iframes' },
|
|
378
408
|
{ name: 'wait-until', default: 'domstable', choices: ['domstable', 'networkidle'], help: 'Readiness policy after navigation: domstable or networkidle' },
|
|
379
|
-
{ name: 'frames', default: 'same-origin', choices: ['same-origin', 'none'], help: 'Iframe handling mode: same-origin or none' },
|
|
409
|
+
{ name: 'frames', default: 'same-origin', choices: ['same-origin', 'all-same-origin', 'none'], help: 'Iframe handling mode: relevant same-origin, all-same-origin, or none' },
|
|
380
410
|
{ name: 'diagnose', type: 'boolean', default: false, help: 'Print render diagnostics (frames, empty containers, XHR/API-like requests) to stderr' },
|
|
381
411
|
{ name: 'stdout', type: 'boolean', default: false, help: 'Print markdown to stdout instead of saving to a file' },
|
|
382
412
|
],
|
package/clis/web/read.test.js
CHANGED
|
@@ -165,6 +165,18 @@ describe('web/read stdout behavior', () => {
|
|
|
165
165
|
expect(page.evaluate.mock.calls[0]?.[0]).toContain('const frameMode = "none"');
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
it('passes --frames all-same-origin into the extractor', async () => {
|
|
169
|
+
await read.func(page, {
|
|
170
|
+
url: 'https://example.com/article',
|
|
171
|
+
output: '/tmp/out',
|
|
172
|
+
'download-images': false,
|
|
173
|
+
frames: 'all-same-origin',
|
|
174
|
+
stdout: false,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(page.evaluate.mock.calls[0]?.[0]).toContain('const frameMode = "all-same-origin"');
|
|
178
|
+
});
|
|
179
|
+
|
|
168
180
|
it('fails fast when --wait-until networkidle is requested but capture is unavailable', async () => {
|
|
169
181
|
page.startNetworkCapture.mockResolvedValue(false);
|
|
170
182
|
|
|
@@ -217,7 +229,7 @@ describe('web/read render-aware helpers', () => {
|
|
|
217
229
|
`, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
|
|
218
230
|
const frame = dom.window.document.querySelector('iframe');
|
|
219
231
|
frame.contentDocument.open();
|
|
220
|
-
frame.contentDocument.write('<body><table id="gridHd"><tr><th>Name</th></tr></table><ul id="gridDatas"><
|
|
232
|
+
frame.contentDocument.write('<body><table id="gridHd"><tr><th>Name</th></tr></table><ul id="gridDatas"></ul><p>Station A 42</p></body>');
|
|
221
233
|
frame.contentDocument.close();
|
|
222
234
|
|
|
223
235
|
const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'same-origin' }));
|
|
@@ -226,6 +238,94 @@ describe('web/read render-aware helpers', () => {
|
|
|
226
238
|
expect(result.contentHtml).toContain('data-opencli-iframe-source="https://example.com/frame.html"');
|
|
227
239
|
expect(result.contentHtml).toContain('来自 iframe: https://example.com/frame.html');
|
|
228
240
|
expect(result.contentHtml).toContain('Station A 42');
|
|
241
|
+
expect(result.diagnostics.emptyContainers).toEqual(expect.arrayContaining([
|
|
242
|
+
expect.objectContaining({ scope: 'iframe', id: 'gridDatas', url: 'https://example.com/frame.html' }),
|
|
243
|
+
]));
|
|
244
|
+
expect(result.diagnostics.emptyContainers.every(item => item.scope === 'iframe')).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('merges readable same-origin iframes outside the selected content element', () => {
|
|
248
|
+
const dom = new JSDOM(`
|
|
249
|
+
<main>
|
|
250
|
+
<h1>Main Article</h1>
|
|
251
|
+
<p>${'Main content '.repeat(30)}</p>
|
|
252
|
+
</main>
|
|
253
|
+
<aside>
|
|
254
|
+
<iframe id="outside" src="/outside.html"></iframe>
|
|
255
|
+
</aside>
|
|
256
|
+
`, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
|
|
257
|
+
const frame = dom.window.document.querySelector('iframe');
|
|
258
|
+
frame.contentDocument.open();
|
|
259
|
+
frame.contentDocument.write(`<body><h1>Outside Frame</h1><p>${'Frame data '.repeat(12)}</p></body>`);
|
|
260
|
+
frame.contentDocument.close();
|
|
261
|
+
|
|
262
|
+
const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'same-origin' }));
|
|
263
|
+
|
|
264
|
+
expect(result.diagnostics.includedFrameCount).toBe(1);
|
|
265
|
+
expect(result.contentHtml).toContain('data-opencli-iframe-source="https://example.com/outside.html"');
|
|
266
|
+
expect(result.contentHtml).toContain('Outside Frame');
|
|
267
|
+
expect(result.contentHtml).toContain('Frame data');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('keeps short data-like iframes outside the selected content element', () => {
|
|
271
|
+
const dom = new JSDOM(`
|
|
272
|
+
<main>
|
|
273
|
+
<h1>Main Article</h1>
|
|
274
|
+
<p>${'Main content '.repeat(30)}</p>
|
|
275
|
+
</main>
|
|
276
|
+
<iframe id="data-frame" src="/data.html"></iframe>
|
|
277
|
+
`, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
|
|
278
|
+
const frame = dom.window.document.querySelector('iframe');
|
|
279
|
+
frame.contentDocument.open();
|
|
280
|
+
frame.contentDocument.write('<body><table id="gridHd"><tr><th>水位</th></tr><tr><td>42</td></tr></table><ul id="gridDatas"></ul></body>');
|
|
281
|
+
frame.contentDocument.close();
|
|
282
|
+
|
|
283
|
+
const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'same-origin' }));
|
|
284
|
+
|
|
285
|
+
expect(result.diagnostics.includedFrameCount).toBe(1);
|
|
286
|
+
expect(result.contentHtml).toContain('42');
|
|
287
|
+
expect(result.diagnostics.emptyContainers).toEqual(expect.arrayContaining([
|
|
288
|
+
expect.objectContaining({ scope: 'iframe', id: 'gridDatas', url: 'https://example.com/data.html' }),
|
|
289
|
+
]));
|
|
290
|
+
expect(result.diagnostics.emptyContainers.every(item => item.scope === 'iframe')).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('skips short non-structural iframes outside the selected content element', () => {
|
|
294
|
+
const dom = new JSDOM(`
|
|
295
|
+
<main>
|
|
296
|
+
<h1>Main Article</h1>
|
|
297
|
+
<p>${'Main content '.repeat(30)}</p>
|
|
298
|
+
</main>
|
|
299
|
+
<iframe id="tiny-frame" src="/tiny.html"></iframe>
|
|
300
|
+
`, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
|
|
301
|
+
const frame = dom.window.document.querySelector('iframe');
|
|
302
|
+
frame.contentDocument.open();
|
|
303
|
+
frame.contentDocument.write('<body><p>tiny note</p></body>');
|
|
304
|
+
frame.contentDocument.close();
|
|
305
|
+
|
|
306
|
+
const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'same-origin' }));
|
|
307
|
+
|
|
308
|
+
expect(result.diagnostics.includedFrameCount).toBe(0);
|
|
309
|
+
expect(result.contentHtml).not.toContain('tiny note');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('includes short non-structural iframes in all-same-origin mode', () => {
|
|
313
|
+
const dom = new JSDOM(`
|
|
314
|
+
<main>
|
|
315
|
+
<h1>Main Article</h1>
|
|
316
|
+
<p>${'Main content '.repeat(30)}</p>
|
|
317
|
+
</main>
|
|
318
|
+
<iframe id="status-frame" src="/status.html"></iframe>
|
|
319
|
+
`, { url: 'https://example.com/main.html', runScripts: 'outside-only' });
|
|
320
|
+
const frame = dom.window.document.querySelector('iframe');
|
|
321
|
+
frame.contentDocument.open();
|
|
322
|
+
frame.contentDocument.write('<body><div>Online: 42°C</div></body>');
|
|
323
|
+
frame.contentDocument.close();
|
|
324
|
+
|
|
325
|
+
const result = dom.window.eval(__test__.buildRenderAwareExtractorJs({ frames: 'all-same-origin' }));
|
|
326
|
+
|
|
327
|
+
expect(result.diagnostics.includedFrameCount).toBe(1);
|
|
328
|
+
expect(result.contentHtml).toContain('Online: 42°C');
|
|
229
329
|
});
|
|
230
330
|
|
|
231
331
|
it('marks API-like network entries as interesting and ignores static assets', () => {
|
|
@@ -179,13 +179,13 @@ export const createDraftCommand = cli({
|
|
|
179
179
|
strategy: Strategy.COOKIE,
|
|
180
180
|
browser: true,
|
|
181
181
|
navigateBefore: false,
|
|
182
|
-
timeoutSeconds: 180,
|
|
183
182
|
args: [
|
|
184
183
|
{ name: 'title', required: true, help: '文章标题 (最长64字)' },
|
|
185
184
|
{ name: 'content', required: true, positional: true, help: '文章正文' },
|
|
186
185
|
{ name: 'author', help: '作者名 (最长8字)' },
|
|
187
186
|
{ name: 'cover-image', help: '封面图片路径 (会先上传到正文再设为封面)' },
|
|
188
187
|
{ name: 'summary', help: '文章摘要' },
|
|
188
|
+
{ name: 'timeout', type: 'int', required: false, default: 180, help: 'Max seconds for the overall command (default: 180)' },
|
|
189
189
|
],
|
|
190
190
|
columns: ['status', 'detail'],
|
|
191
191
|
|
package/clis/weixin/drafts.js
CHANGED
|
@@ -12,9 +12,9 @@ export const draftsCommand = cli({
|
|
|
12
12
|
strategy: Strategy.COOKIE,
|
|
13
13
|
browser: true,
|
|
14
14
|
navigateBefore: false,
|
|
15
|
-
timeoutSeconds: 60,
|
|
16
15
|
args: [
|
|
17
16
|
{ name: 'limit', type: 'int', default: 10, help: '最多显示条数' },
|
|
17
|
+
{ name: 'timeout', type: 'int', required: false, default: 60, help: 'Max seconds for the overall command (default: 60)' },
|
|
18
18
|
],
|
|
19
19
|
columns: ['Index', 'Title', 'Time'],
|
|
20
20
|
|
|
@@ -3,6 +3,7 @@ import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
|
3
3
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
4
|
import './create-draft.js';
|
|
5
5
|
import './drafts.js';
|
|
6
|
+
import './search.js';
|
|
6
7
|
|
|
7
8
|
function createPageMock(overrides = {}) {
|
|
8
9
|
return {
|
|
@@ -18,7 +19,10 @@ describe('weixin command registration', () => {
|
|
|
18
19
|
const registry = getRegistry();
|
|
19
20
|
const values = [...registry.values()];
|
|
20
21
|
expect(values.find(c => c.site === 'weixin' && c.name === 'create-draft')).toBeDefined();
|
|
21
|
-
|
|
22
|
+
const draftsCommand = values.find(c => c.site === 'weixin' && c.name === 'drafts');
|
|
23
|
+
expect(draftsCommand).toBeDefined();
|
|
24
|
+
expect(draftsCommand.args.find((arg) => arg.name === 'timeout')).toMatchObject({ type: 'int', default: 60 });
|
|
25
|
+
expect(values.find(c => c.site === 'weixin' && c.name === 'search')).toBeDefined();
|
|
22
26
|
});
|
|
23
27
|
});
|
|
24
28
|
|