@zenalexa/unicli 0.219.0 → 0.220.0
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/AGENTS.md +9 -9
- package/README.md +55 -249
- package/README.zh-CN.md +55 -249
- package/dist/adapters/1point3acres/forum.d.ts +58 -0
- package/dist/adapters/1point3acres/forum.d.ts.map +1 -0
- package/dist/adapters/1point3acres/forum.js +708 -0
- package/dist/adapters/1point3acres/forum.js.map +1 -0
- package/dist/adapters/aibase/news.d.ts +21 -0
- package/dist/adapters/aibase/news.d.ts.map +1 -0
- package/dist/adapters/aibase/news.js +96 -0
- package/dist/adapters/aibase/news.js.map +1 -0
- package/dist/adapters/arxiv/papers.d.ts +27 -0
- package/dist/adapters/arxiv/papers.d.ts.map +1 -0
- package/dist/adapters/arxiv/papers.js +193 -0
- package/dist/adapters/arxiv/papers.js.map +1 -0
- package/dist/adapters/bbc/topic.d.ts +24 -0
- package/dist/adapters/bbc/topic.d.ts.map +1 -0
- package/dist/adapters/bbc/topic.js +122 -0
- package/dist/adapters/bbc/topic.js.map +1 -0
- package/dist/adapters/chatgpt/web.d.ts +24 -0
- package/dist/adapters/chatgpt/web.d.ts.map +1 -0
- package/dist/adapters/chatgpt/web.js +242 -0
- package/dist/adapters/chatgpt/web.js.map +1 -0
- package/dist/adapters/claude/web.d.ts +24 -0
- package/dist/adapters/claude/web.d.ts.map +1 -0
- package/dist/adapters/claude/web.js +575 -0
- package/dist/adapters/claude/web.js.map +1 -0
- package/dist/adapters/codex/projects.d.ts +27 -0
- package/dist/adapters/codex/projects.d.ts.map +1 -0
- package/dist/adapters/codex/projects.js +147 -0
- package/dist/adapters/codex/projects.js.map +1 -0
- package/dist/adapters/coingecko/markets.d.ts +19 -0
- package/dist/adapters/coingecko/markets.d.ts.map +1 -0
- package/dist/adapters/coingecko/markets.js +474 -0
- package/dist/adapters/coingecko/markets.js.map +1 -0
- package/dist/adapters/coupang/product.d.ts +27 -0
- package/dist/adapters/coupang/product.d.ts.map +1 -0
- package/dist/adapters/coupang/product.js +211 -0
- package/dist/adapters/coupang/product.js.map +1 -0
- package/dist/adapters/crates/registry.d.ts +44 -0
- package/dist/adapters/crates/registry.d.ts.map +1 -0
- package/dist/adapters/crates/registry.js +186 -0
- package/dist/adapters/crates/registry.js.map +1 -0
- package/dist/adapters/ctrip/travel.d.ts +83 -0
- package/dist/adapters/ctrip/travel.d.ts.map +1 -0
- package/dist/adapters/ctrip/travel.js +630 -0
- package/dist/adapters/ctrip/travel.js.map +1 -0
- package/dist/adapters/dblp/publications.d.ts +41 -0
- package/dist/adapters/dblp/publications.d.ts.map +1 -0
- package/dist/adapters/dblp/publications.js +409 -0
- package/dist/adapters/dblp/publications.js.map +1 -0
- package/dist/adapters/deepseek/web.d.ts +1 -1
- package/dist/adapters/deepseek/web.d.ts.map +1 -1
- package/dist/adapters/deepseek/web.js +66 -1
- package/dist/adapters/deepseek/web.js.map +1 -1
- package/dist/adapters/defillama/protocols.d.ts +13 -0
- package/dist/adapters/defillama/protocols.d.ts.map +1 -0
- package/dist/adapters/defillama/protocols.js +218 -0
- package/dist/adapters/defillama/protocols.js.map +1 -0
- package/dist/adapters/devto/read.d.ts +26 -0
- package/dist/adapters/devto/read.d.ts.map +1 -0
- package/dist/adapters/devto/read.js +110 -0
- package/dist/adapters/devto/read.js.map +1 -0
- package/dist/adapters/dianping/shop.d.ts +38 -0
- package/dist/adapters/dianping/shop.d.ts.map +1 -0
- package/dist/adapters/dianping/shop.js +194 -0
- package/dist/adapters/dianping/shop.js.map +1 -0
- package/dist/adapters/dockerhub/registry.d.ts +36 -0
- package/dist/adapters/dockerhub/registry.d.ts.map +1 -0
- package/dist/adapters/dockerhub/registry.js +172 -0
- package/dist/adapters/dockerhub/registry.js.map +1 -0
- package/dist/adapters/endoflife/product.d.ts +11 -0
- package/dist/adapters/endoflife/product.d.ts.map +1 -0
- package/dist/adapters/endoflife/product.js +113 -0
- package/dist/adapters/endoflife/product.js.map +1 -0
- package/dist/adapters/facebook/marketplace-extra.d.ts +9 -0
- package/dist/adapters/facebook/marketplace-extra.d.ts.map +1 -0
- package/dist/adapters/facebook/marketplace-extra.js +170 -0
- package/dist/adapters/facebook/marketplace-extra.js.map +1 -0
- package/dist/adapters/flathub/apps.d.ts +17 -0
- package/dist/adapters/flathub/apps.d.ts.map +1 -0
- package/dist/adapters/flathub/apps.js +220 -0
- package/dist/adapters/flathub/apps.js.map +1 -0
- package/dist/adapters/goproxy/module.d.ts +24 -0
- package/dist/adapters/goproxy/module.d.ts.map +1 -0
- package/dist/adapters/goproxy/module.js +221 -0
- package/dist/adapters/goproxy/module.js.map +1 -0
- package/dist/adapters/grok/web.d.ts +29 -0
- package/dist/adapters/grok/web.d.ts.map +1 -0
- package/dist/adapters/grok/web.js +553 -0
- package/dist/adapters/grok/web.js.map +1 -0
- package/dist/adapters/hackernews/read.d.ts +31 -0
- package/dist/adapters/hackernews/read.d.ts.map +1 -0
- package/dist/adapters/hackernews/read.js +201 -0
- package/dist/adapters/hackernews/read.js.map +1 -0
- package/dist/adapters/hf/paper.d.ts +22 -0
- package/dist/adapters/hf/paper.d.ts.map +1 -0
- package/dist/adapters/hf/paper.js +112 -0
- package/dist/adapters/hf/paper.js.map +1 -0
- package/dist/adapters/homebrew/packages.d.ts +52 -0
- package/dist/adapters/homebrew/packages.d.ts.map +1 -0
- package/dist/adapters/homebrew/packages.js +240 -0
- package/dist/adapters/homebrew/packages.js.map +1 -0
- package/dist/adapters/indeed/jobs.d.ts +38 -0
- package/dist/adapters/indeed/jobs.d.ts.map +1 -0
- package/dist/adapters/indeed/jobs.js +300 -0
- package/dist/adapters/indeed/jobs.js.map +1 -0
- package/dist/adapters/instagram/collections.d.ts +9 -0
- package/dist/adapters/instagram/collections.d.ts.map +1 -0
- package/dist/adapters/instagram/collections.js +174 -0
- package/dist/adapters/instagram/collections.js.map +1 -0
- package/dist/adapters/lichess/players.d.ts +46 -0
- package/dist/adapters/lichess/players.d.ts.map +1 -0
- package/dist/adapters/lichess/players.js +221 -0
- package/dist/adapters/lichess/players.js.map +1 -0
- package/dist/adapters/lobsters/read-domain.d.ts +35 -0
- package/dist/adapters/lobsters/read-domain.d.ts.map +1 -0
- package/dist/adapters/lobsters/read-domain.js +306 -0
- package/dist/adapters/lobsters/read-domain.js.map +1 -0
- package/dist/adapters/maven/artifact.d.ts +30 -0
- package/dist/adapters/maven/artifact.d.ts.map +1 -0
- package/dist/adapters/maven/artifact.js +121 -0
- package/dist/adapters/maven/artifact.js.map +1 -0
- package/dist/adapters/mdn/search.d.ts +11 -0
- package/dist/adapters/mdn/search.d.ts.map +1 -0
- package/dist/adapters/mdn/search.js +115 -0
- package/dist/adapters/mdn/search.js.map +1 -0
- package/dist/adapters/medium/tag.d.ts +15 -0
- package/dist/adapters/medium/tag.d.ts.map +1 -0
- package/dist/adapters/medium/tag.js +148 -0
- package/dist/adapters/medium/tag.js.map +1 -0
- package/dist/adapters/npm/package.d.ts +32 -0
- package/dist/adapters/npm/package.d.ts.map +1 -0
- package/dist/adapters/npm/package.js +141 -0
- package/dist/adapters/npm/package.js.map +1 -0
- package/dist/adapters/nuget/package.d.ts +34 -0
- package/dist/adapters/nuget/package.d.ts.map +1 -0
- package/dist/adapters/nuget/package.js +135 -0
- package/dist/adapters/nuget/package.js.map +1 -0
- package/dist/adapters/nvd/cve.d.ts +42 -0
- package/dist/adapters/nvd/cve.d.ts.map +1 -0
- package/dist/adapters/nvd/cve.js +132 -0
- package/dist/adapters/nvd/cve.js.map +1 -0
- package/dist/adapters/oeis/sequences.d.ts +14 -0
- package/dist/adapters/oeis/sequences.d.ts.map +1 -0
- package/dist/adapters/oeis/sequences.js +219 -0
- package/dist/adapters/oeis/sequences.js.map +1 -0
- package/dist/adapters/openalex/works.d.ts +43 -0
- package/dist/adapters/openalex/works.d.ts.map +1 -0
- package/dist/adapters/openalex/works.js +267 -0
- package/dist/adapters/openalex/works.js.map +1 -0
- package/dist/adapters/openfda/records.d.ts +18 -0
- package/dist/adapters/openfda/records.d.ts.map +1 -0
- package/dist/adapters/openfda/records.js +209 -0
- package/dist/adapters/openfda/records.js.map +1 -0
- package/dist/adapters/openreview/papers.d.ts +34 -0
- package/dist/adapters/openreview/papers.d.ts.map +1 -0
- package/dist/adapters/openreview/papers.js +463 -0
- package/dist/adapters/openreview/papers.js.map +1 -0
- package/dist/adapters/osv/security.d.ts +36 -0
- package/dist/adapters/osv/security.d.ts.map +1 -0
- package/dist/adapters/osv/security.js +247 -0
- package/dist/adapters/osv/security.js.map +1 -0
- package/dist/adapters/packagist/package.d.ts +31 -0
- package/dist/adapters/packagist/package.d.ts.map +1 -0
- package/dist/adapters/packagist/package.js +108 -0
- package/dist/adapters/packagist/package.js.map +1 -0
- package/dist/adapters/pubmed/articles.d.ts +31 -0
- package/dist/adapters/pubmed/articles.d.ts.map +1 -0
- package/dist/adapters/pubmed/articles.js +385 -0
- package/dist/adapters/pubmed/articles.js.map +1 -0
- package/dist/adapters/pypi/package.d.ts +38 -0
- package/dist/adapters/pypi/package.d.ts.map +1 -0
- package/dist/adapters/pypi/package.js +235 -0
- package/dist/adapters/pypi/package.js.map +1 -0
- package/dist/adapters/qwen/web.d.ts +26 -0
- package/dist/adapters/qwen/web.d.ts.map +1 -0
- package/dist/adapters/qwen/web.js +672 -0
- package/dist/adapters/qwen/web.js.map +1 -0
- package/dist/adapters/reddit/account.d.ts +12 -0
- package/dist/adapters/reddit/account.d.ts.map +1 -0
- package/dist/adapters/reddit/account.js +409 -0
- package/dist/adapters/reddit/account.js.map +1 -0
- package/dist/adapters/rednote/web.d.ts +30 -0
- package/dist/adapters/rednote/web.d.ts.map +1 -0
- package/dist/adapters/rednote/web.js +858 -0
- package/dist/adapters/rednote/web.js.map +1 -0
- package/dist/adapters/rest-countries/countries.d.ts +14 -0
- package/dist/adapters/rest-countries/countries.d.ts.map +1 -0
- package/dist/adapters/rest-countries/countries.js +231 -0
- package/dist/adapters/rest-countries/countries.js.map +1 -0
- package/dist/adapters/reuters/article-detail.d.ts +37 -0
- package/dist/adapters/reuters/article-detail.d.ts.map +1 -0
- package/dist/adapters/reuters/article-detail.js +139 -0
- package/dist/adapters/reuters/article-detail.js.map +1 -0
- package/dist/adapters/rfc/rfc.d.ts +11 -0
- package/dist/adapters/rfc/rfc.d.ts.map +1 -0
- package/dist/adapters/rfc/rfc.js +121 -0
- package/dist/adapters/rfc/rfc.js.map +1 -0
- package/dist/adapters/rubygems/gem.d.ts +26 -0
- package/dist/adapters/rubygems/gem.d.ts.map +1 -0
- package/dist/adapters/rubygems/gem.js +96 -0
- package/dist/adapters/rubygems/gem.js.map +1 -0
- package/dist/adapters/stackoverflow/questions.d.ts +79 -0
- package/dist/adapters/stackoverflow/questions.d.ts.map +1 -0
- package/dist/adapters/stackoverflow/questions.js +504 -0
- package/dist/adapters/stackoverflow/questions.js.map +1 -0
- package/dist/adapters/steam/app.d.ts +39 -0
- package/dist/adapters/steam/app.d.ts.map +1 -0
- package/dist/adapters/steam/app.js +165 -0
- package/dist/adapters/steam/app.js.map +1 -0
- package/dist/adapters/tiktok/creator-videos.d.ts +52 -0
- package/dist/adapters/tiktok/creator-videos.d.ts.map +1 -0
- package/dist/adapters/tiktok/creator-videos.js +267 -0
- package/dist/adapters/tiktok/creator-videos.js.map +1 -0
- package/dist/adapters/tvmaze/shows.d.ts +13 -0
- package/dist/adapters/tvmaze/shows.d.ts.map +1 -0
- package/dist/adapters/tvmaze/shows.js +240 -0
- package/dist/adapters/tvmaze/shows.js.map +1 -0
- package/dist/adapters/twitter/bookmark-folders.d.ts +33 -0
- package/dist/adapters/twitter/bookmark-folders.d.ts.map +1 -0
- package/dist/adapters/twitter/bookmark-folders.js +290 -0
- package/dist/adapters/twitter/bookmark-folders.js.map +1 -0
- package/dist/adapters/twitter/quote.d.ts +18 -0
- package/dist/adapters/twitter/quote.d.ts.map +1 -0
- package/dist/adapters/twitter/quote.js +285 -0
- package/dist/adapters/twitter/quote.js.map +1 -0
- package/dist/adapters/twitter/tweet-actions.d.ts +12 -0
- package/dist/adapters/twitter/tweet-actions.d.ts.map +1 -0
- package/dist/adapters/twitter/tweet-actions.js +145 -0
- package/dist/adapters/twitter/tweet-actions.js.map +1 -0
- package/dist/adapters/twitter/tweet-url.d.ts +15 -0
- package/dist/adapters/twitter/tweet-url.d.ts.map +1 -0
- package/dist/adapters/twitter/tweet-url.js +57 -0
- package/dist/adapters/twitter/tweet-url.js.map +1 -0
- package/dist/adapters/uisdc/news.d.ts +22 -0
- package/dist/adapters/uisdc/news.d.ts.map +1 -0
- package/dist/adapters/uisdc/news.js +91 -0
- package/dist/adapters/uisdc/news.js.map +1 -0
- package/dist/adapters/weibo/favorites-publish.d.ts +28 -0
- package/dist/adapters/weibo/favorites-publish.d.ts.map +1 -0
- package/dist/adapters/weibo/favorites-publish.js +356 -0
- package/dist/adapters/weibo/favorites-publish.js.map +1 -0
- package/dist/adapters/wikidata/entities.d.ts +15 -0
- package/dist/adapters/wikidata/entities.d.ts.map +1 -0
- package/dist/adapters/wikidata/entities.js +219 -0
- package/dist/adapters/wikidata/entities.js.map +1 -0
- package/dist/adapters/wikipedia/page.d.ts +21 -0
- package/dist/adapters/wikipedia/page.d.ts.map +1 -0
- package/dist/adapters/wikipedia/page.js +116 -0
- package/dist/adapters/wikipedia/page.js.map +1 -0
- package/dist/adapters/wttr/weather.d.ts +12 -0
- package/dist/adapters/wttr/weather.d.ts.map +1 -0
- package/dist/adapters/wttr/weather.js +207 -0
- package/dist/adapters/wttr/weather.js.map +1 -0
- package/dist/adapters/xianyu/publish.d.ts +31 -0
- package/dist/adapters/xianyu/publish.d.ts.map +1 -0
- package/dist/adapters/xianyu/publish.js +349 -0
- package/dist/adapters/xianyu/publish.js.map +1 -0
- package/dist/adapters/xiaohongshu/user-helpers.d.ts +2 -2
- package/dist/adapters/xiaohongshu/user-helpers.d.ts.map +1 -1
- package/dist/adapters/xiaohongshu/user-helpers.js +5 -4
- package/dist/adapters/xiaohongshu/user-helpers.js.map +1 -1
- package/dist/adapters/yuanbao/web.d.ts +27 -0
- package/dist/adapters/yuanbao/web.d.ts.map +1 -0
- package/dist/adapters/yuanbao/web.js +365 -0
- package/dist/adapters/yuanbao/web.js.map +1 -0
- package/dist/adapters/zhihu/collection.d.ts +33 -0
- package/dist/adapters/zhihu/collection.d.ts.map +1 -0
- package/dist/adapters/zhihu/collection.js +185 -0
- package/dist/adapters/zhihu/collection.js.map +1 -0
- package/dist/adapters/zlibrary/web.d.ts +19 -0
- package/dist/adapters/zlibrary/web.d.ts.map +1 -0
- package/dist/adapters/zlibrary/web.js +153 -0
- package/dist/adapters/zlibrary/web.js.map +1 -0
- package/dist/browser/daemon-client.js +2 -2
- package/dist/browser/daemon-client.js.map +1 -1
- package/dist/discovery/macos-dynamic.d.ts.map +1 -1
- package/dist/discovery/macos-dynamic.js +17 -3
- package/dist/discovery/macos-dynamic.js.map +1 -1
- package/dist/manifest-compact.txt +10 -10
- package/dist/manifest-search.json +1 -1
- package/dist/manifest.json +5130 -112
- package/dist/transport/refs.d.ts +5 -3
- package/dist/transport/refs.d.ts.map +1 -1
- package/dist/transport/refs.js +8 -1
- package/dist/transport/refs.js.map +1 -1
- package/dist/transport/sidecar-binary.d.ts +7 -0
- package/dist/transport/sidecar-binary.d.ts.map +1 -1
- package/dist/transport/sidecar-binary.js +28 -8
- package/dist/transport/sidecar-binary.js.map +1 -1
- package/package.json +4 -3
- package/server.json +3 -3
- package/skills/unicli/SKILL.md +1 -1
- package/skills/unicli-claude-code/SKILL.md +1 -1
- package/skills/unicli-hermes/SKILL.md +1 -1
- package/src/adapters/1point3acres/forum.test.ts +300 -0
- package/src/adapters/1point3acres/forum.ts +852 -0
- package/src/adapters/aibase/news.test.ts +42 -0
- package/src/adapters/aibase/news.ts +118 -0
- package/src/adapters/arxiv/papers.test.ts +59 -0
- package/src/adapters/arxiv/papers.ts +226 -0
- package/src/adapters/bbc/topic.test.ts +52 -0
- package/src/adapters/bbc/topic.ts +149 -0
- package/src/adapters/chatgpt/web.test.ts +121 -0
- package/src/adapters/chatgpt/web.ts +286 -0
- package/src/adapters/claude/web.test.ts +206 -0
- package/src/adapters/claude/web.ts +684 -0
- package/src/adapters/codex/projects.test.ts +77 -0
- package/src/adapters/codex/projects.ts +178 -0
- package/src/adapters/coingecko/markets.test.ts +156 -0
- package/src/adapters/coingecko/markets.ts +574 -0
- package/src/adapters/coupang/product.test.ts +111 -0
- package/src/adapters/coupang/product.ts +256 -0
- package/src/adapters/crates/registry.test.ts +89 -0
- package/src/adapters/crates/registry.ts +247 -0
- package/src/adapters/ctrip/travel.test.ts +359 -0
- package/src/adapters/ctrip/travel.ts +792 -0
- package/src/adapters/dblp/publications.test.ts +123 -0
- package/src/adapters/dblp/publications.ts +494 -0
- package/src/adapters/deepseek/web.test.ts +69 -0
- package/src/adapters/deepseek/web.ts +78 -1
- package/src/adapters/defillama/protocols.test.ts +75 -0
- package/src/adapters/defillama/protocols.ts +253 -0
- package/src/adapters/devto/read.test.ts +49 -0
- package/src/adapters/devto/read.ts +145 -0
- package/src/adapters/dianping/shop.test.ts +134 -0
- package/src/adapters/dianping/shop.ts +261 -0
- package/src/adapters/dockerhub/registry.test.ts +97 -0
- package/src/adapters/dockerhub/registry.ts +223 -0
- package/src/adapters/endoflife/product.test.ts +50 -0
- package/src/adapters/endoflife/product.ts +128 -0
- package/src/adapters/facebook/marketplace-extra.test.ts +132 -0
- package/src/adapters/facebook/marketplace-extra.ts +213 -0
- package/src/adapters/flathub/apps.test.ts +85 -0
- package/src/adapters/flathub/apps.ts +254 -0
- package/src/adapters/goproxy/module.test.ts +72 -0
- package/src/adapters/goproxy/module.ts +258 -0
- package/src/adapters/grok/web.test.ts +181 -0
- package/src/adapters/grok/web.ts +640 -0
- package/src/adapters/hackernews/read.test.ts +68 -0
- package/src/adapters/hackernews/read.ts +265 -0
- package/src/adapters/hf/paper.test.ts +48 -0
- package/src/adapters/hf/paper.ts +138 -0
- package/src/adapters/homebrew/packages.test.ts +109 -0
- package/src/adapters/homebrew/packages.ts +304 -0
- package/src/adapters/indeed/jobs.test.ts +230 -0
- package/src/adapters/indeed/jobs.ts +375 -0
- package/src/adapters/instagram/collections.test.ts +94 -0
- package/src/adapters/instagram/collections.ts +206 -0
- package/src/adapters/lichess/players.test.ts +99 -0
- package/src/adapters/lichess/players.ts +277 -0
- package/src/adapters/lobsters/read-domain.test.ts +121 -0
- package/src/adapters/lobsters/read-domain.ts +400 -0
- package/src/adapters/maven/artifact.test.ts +67 -0
- package/src/adapters/maven/artifact.ts +155 -0
- package/src/adapters/mdn/search.test.ts +39 -0
- package/src/adapters/mdn/search.ts +133 -0
- package/src/adapters/medium/tag.test.ts +64 -0
- package/src/adapters/medium/tag.ts +164 -0
- package/src/adapters/npm/package.test.ts +53 -0
- package/src/adapters/npm/package.ts +177 -0
- package/src/adapters/nuget/package.test.ts +102 -0
- package/src/adapters/nuget/package.ts +193 -0
- package/src/adapters/nvd/cve.test.ts +66 -0
- package/src/adapters/nvd/cve.ts +182 -0
- package/src/adapters/oeis/sequences.test.ts +71 -0
- package/src/adapters/oeis/sequences.ts +234 -0
- package/src/adapters/openalex/works.test.ts +99 -0
- package/src/adapters/openalex/works.ts +319 -0
- package/src/adapters/openfda/records.test.ts +90 -0
- package/src/adapters/openfda/records.ts +239 -0
- package/src/adapters/openreview/papers.test.ts +139 -0
- package/src/adapters/openreview/papers.ts +560 -0
- package/src/adapters/osv/security.test.ts +91 -0
- package/src/adapters/osv/security.ts +298 -0
- package/src/adapters/packagist/package.test.ts +62 -0
- package/src/adapters/packagist/package.ts +146 -0
- package/src/adapters/pubmed/articles.test.ts +96 -0
- package/src/adapters/pubmed/articles.ts +497 -0
- package/src/adapters/pypi/package.test.ts +131 -0
- package/src/adapters/pypi/package.ts +297 -0
- package/src/adapters/qwen/web.test.ts +176 -0
- package/src/adapters/qwen/web.ts +758 -0
- package/src/adapters/reddit/account.test.ts +56 -0
- package/src/adapters/reddit/account.ts +493 -0
- package/src/adapters/rednote/web.test.ts +354 -0
- package/src/adapters/rednote/web.ts +968 -0
- package/src/adapters/rest-countries/countries.test.ts +80 -0
- package/src/adapters/rest-countries/countries.ts +271 -0
- package/src/adapters/reuters/article-detail.test.ts +65 -0
- package/src/adapters/reuters/article-detail.ts +186 -0
- package/src/adapters/rfc/rfc.test.ts +37 -0
- package/src/adapters/rfc/rfc.ts +133 -0
- package/src/adapters/rubygems/gem.test.ts +43 -0
- package/src/adapters/rubygems/gem.ts +126 -0
- package/src/adapters/stackoverflow/questions.test.ts +207 -0
- package/src/adapters/stackoverflow/questions.ts +765 -0
- package/src/adapters/steam/app.test.ts +68 -0
- package/src/adapters/steam/app.ts +218 -0
- package/src/adapters/tiktok/creator-videos.test.ts +158 -0
- package/src/adapters/tiktok/creator-videos.ts +370 -0
- package/src/adapters/tvmaze/shows.test.ts +93 -0
- package/src/adapters/tvmaze/shows.ts +271 -0
- package/src/adapters/twitter/bookmark-folders.test.ts +164 -0
- package/src/adapters/twitter/bookmark-folders.ts +366 -0
- package/src/adapters/twitter/quote.test.ts +157 -0
- package/src/adapters/twitter/quote.ts +332 -0
- package/src/adapters/twitter/tweet-actions.test.ts +51 -0
- package/src/adapters/twitter/tweet-actions.ts +187 -0
- package/src/adapters/twitter/tweet-url.ts +65 -0
- package/src/adapters/uisdc/news.test.ts +46 -0
- package/src/adapters/uisdc/news.ts +111 -0
- package/src/adapters/weibo/favorites-publish.test.ts +177 -0
- package/src/adapters/weibo/favorites-publish.ts +426 -0
- package/src/adapters/wikidata/entities.test.ts +103 -0
- package/src/adapters/wikidata/entities.ts +253 -0
- package/src/adapters/wikipedia/page.test.ts +49 -0
- package/src/adapters/wikipedia/page.ts +158 -0
- package/src/adapters/wttr/weather.test.ts +99 -0
- package/src/adapters/wttr/weather.ts +239 -0
- package/src/adapters/xianyu/publish.test.ts +210 -0
- package/src/adapters/xianyu/publish.ts +420 -0
- package/src/adapters/xiaohongshu/user-helpers.ts +5 -2
- package/src/adapters/yuanbao/web.test.ts +144 -0
- package/src/adapters/yuanbao/web.ts +435 -0
- package/src/adapters/zhihu/collection.test.ts +96 -0
- package/src/adapters/zhihu/collection.ts +239 -0
- package/src/adapters/zlibrary/web.test.ts +104 -0
- package/src/adapters/zlibrary/web.ts +192 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owner src/adapters/ctrip/travel.ts
|
|
3
|
+
* @does Register agent-facing Ctrip destination, hotel, and flight commands.
|
|
4
|
+
* @needs Public Ctrip suggest endpoint plus logged-in or challenge-cleared browser sessions for hotel and flight pages.
|
|
5
|
+
* @feeds surface coverage ledger, travel search workflows, and Ctrip active command discovery.
|
|
6
|
+
* @breaks Ctrip suggest response drift, SSR hotel-list changes, flight-card DOM drift, or captcha gating.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cli, Strategy } from "../../registry.js";
|
|
10
|
+
import type { IPage } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
const SUGGEST_ENDPOINT =
|
|
13
|
+
"https://m.ctrip.com/restapi/soa2/21881/json/gaHotelSearchEngine";
|
|
14
|
+
const SUGGEST_DEFAULT_LIMIT = 15;
|
|
15
|
+
const SUGGEST_MAX_LIMIT = 50;
|
|
16
|
+
const HOTEL_DEFAULT_LIMIT = 10;
|
|
17
|
+
const HOTEL_MAX_LIMIT = 30;
|
|
18
|
+
const FLIGHT_DEFAULT_LIMIT = 20;
|
|
19
|
+
const FLIGHT_MAX_LIMIT = 50;
|
|
20
|
+
|
|
21
|
+
export const CTRIP_SUGGEST_COLUMNS = [
|
|
22
|
+
"rank",
|
|
23
|
+
"id",
|
|
24
|
+
"type",
|
|
25
|
+
"displayType",
|
|
26
|
+
"name",
|
|
27
|
+
"eName",
|
|
28
|
+
"cityId",
|
|
29
|
+
"cityName",
|
|
30
|
+
"provinceName",
|
|
31
|
+
"countryName",
|
|
32
|
+
"lat",
|
|
33
|
+
"lon",
|
|
34
|
+
"score",
|
|
35
|
+
"url",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const HOTEL_COLUMNS = [
|
|
39
|
+
"rank",
|
|
40
|
+
"hotelId",
|
|
41
|
+
"name",
|
|
42
|
+
"enName",
|
|
43
|
+
"star",
|
|
44
|
+
"score",
|
|
45
|
+
"scoreLabel",
|
|
46
|
+
"reviewCount",
|
|
47
|
+
"cityName",
|
|
48
|
+
"district",
|
|
49
|
+
"address",
|
|
50
|
+
"lat",
|
|
51
|
+
"lon",
|
|
52
|
+
"price",
|
|
53
|
+
"currency",
|
|
54
|
+
"url",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const FLIGHT_COLUMNS = [
|
|
58
|
+
"rank",
|
|
59
|
+
"airline",
|
|
60
|
+
"flightNo",
|
|
61
|
+
"aircraft",
|
|
62
|
+
"departureTime",
|
|
63
|
+
"departureAirport",
|
|
64
|
+
"arrivalTime",
|
|
65
|
+
"arrivalAirport",
|
|
66
|
+
"terminal",
|
|
67
|
+
"price",
|
|
68
|
+
"currency",
|
|
69
|
+
"cabin",
|
|
70
|
+
"url",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
interface CtripSuggestItem {
|
|
74
|
+
id?: unknown;
|
|
75
|
+
type?: unknown;
|
|
76
|
+
word?: unknown;
|
|
77
|
+
cityId?: unknown;
|
|
78
|
+
cityName?: unknown;
|
|
79
|
+
provinceName?: unknown;
|
|
80
|
+
countryName?: unknown;
|
|
81
|
+
displayName?: unknown;
|
|
82
|
+
displayType?: unknown;
|
|
83
|
+
eName?: unknown;
|
|
84
|
+
commentScore?: unknown;
|
|
85
|
+
cStar?: unknown;
|
|
86
|
+
gdLat?: unknown;
|
|
87
|
+
gdLon?: unknown;
|
|
88
|
+
gLat?: unknown;
|
|
89
|
+
gLon?: unknown;
|
|
90
|
+
lat?: unknown;
|
|
91
|
+
lon?: unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface CtripHotelEntry {
|
|
95
|
+
hotelInfo?: {
|
|
96
|
+
summary?: { hotelId?: unknown };
|
|
97
|
+
nameInfo?: { name?: unknown; enName?: unknown };
|
|
98
|
+
hotelStar?: { star?: unknown };
|
|
99
|
+
commentInfo?: {
|
|
100
|
+
commentScore?: unknown;
|
|
101
|
+
commentDescription?: unknown;
|
|
102
|
+
commenterNumber?: unknown;
|
|
103
|
+
};
|
|
104
|
+
positionInfo?: {
|
|
105
|
+
cityName?: unknown;
|
|
106
|
+
positionDesc?: unknown;
|
|
107
|
+
address?: unknown;
|
|
108
|
+
mapCoordinate?: unknown;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
roomInfo?: Array<{ priceInfo?: { price?: unknown; currency?: unknown } }>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface CtripFlightRow {
|
|
115
|
+
airline?: unknown;
|
|
116
|
+
flightNo?: unknown;
|
|
117
|
+
aircraft?: unknown;
|
|
118
|
+
departureTime?: unknown;
|
|
119
|
+
departureAirport?: unknown;
|
|
120
|
+
arrivalTime?: unknown;
|
|
121
|
+
arrivalAirport?: unknown;
|
|
122
|
+
terminal?: unknown;
|
|
123
|
+
price?: unknown;
|
|
124
|
+
currency?: unknown;
|
|
125
|
+
cabin?: unknown;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stringValue(value: unknown): string {
|
|
129
|
+
return String(value ?? "")
|
|
130
|
+
.replace(/\s+/g, " ")
|
|
131
|
+
.trim();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function nullableString(value: unknown): string | null {
|
|
135
|
+
const text = stringValue(value);
|
|
136
|
+
return text || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function finiteNonZero(value: unknown): number | null {
|
|
140
|
+
const numberValue = Number(value);
|
|
141
|
+
return Number.isFinite(numberValue) && numberValue !== 0 ? numberValue : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function parseCtripLimit(
|
|
145
|
+
raw: unknown,
|
|
146
|
+
fallback = SUGGEST_DEFAULT_LIMIT,
|
|
147
|
+
maxValue = SUGGEST_MAX_LIMIT,
|
|
148
|
+
): number {
|
|
149
|
+
if (raw === undefined || raw === null || raw === "") return fallback;
|
|
150
|
+
const parsed = Number(raw);
|
|
151
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
|
152
|
+
throw new Error(`--limit must be an integer between 1 and ${maxValue}.`);
|
|
153
|
+
}
|
|
154
|
+
if (parsed < 1 || parsed > maxValue) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`--limit must be between 1 and ${maxValue}, got ${parsed}.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return parsed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function requireCtripQuery(value: unknown): string {
|
|
163
|
+
const query = stringValue(value);
|
|
164
|
+
if (!query) throw new Error("Search keyword cannot be empty.");
|
|
165
|
+
return query;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function parseCtripIsoDate(name: string, raw: unknown): string {
|
|
169
|
+
const value = String(raw ?? "").trim();
|
|
170
|
+
if (!value) throw new Error(`--${name} is required (YYYY-MM-DD).`);
|
|
171
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
172
|
+
if (!match) throw new Error(`--${name} must be YYYY-MM-DD.`);
|
|
173
|
+
const year = Number(match[1]);
|
|
174
|
+
const month = Number(match[2]);
|
|
175
|
+
const day = Number(match[3]);
|
|
176
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
177
|
+
throw new Error(`--${name} has invalid month/day: ${value}.`);
|
|
178
|
+
}
|
|
179
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
180
|
+
if (
|
|
181
|
+
parsed.getUTCFullYear() !== year ||
|
|
182
|
+
parsed.getUTCMonth() !== month - 1 ||
|
|
183
|
+
parsed.getUTCDate() !== day
|
|
184
|
+
) {
|
|
185
|
+
throw new Error(`--${name} is not a real calendar date: ${value}.`);
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function parseCtripIataCode(name: string, raw: unknown): string {
|
|
191
|
+
const value = String(raw ?? "")
|
|
192
|
+
.trim()
|
|
193
|
+
.toUpperCase();
|
|
194
|
+
if (!value) throw new Error(`--${name} is required (3-letter IATA code).`);
|
|
195
|
+
if (!/^[A-Z]{3}$/.test(value)) {
|
|
196
|
+
throw new Error(`--${name} must be a 3-letter IATA code.`);
|
|
197
|
+
}
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function parseCtripCityId(raw: unknown): number {
|
|
202
|
+
const value = String(raw ?? "").trim();
|
|
203
|
+
if (!value) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"--city is required (numeric city ID from ctrip search or hotel-suggest).",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const parsed = Number(value);
|
|
209
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
|
|
210
|
+
throw new Error(`--city must be a positive integer city ID, got ${value}.`);
|
|
211
|
+
}
|
|
212
|
+
return parsed;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function assertCtripCheckinBeforeCheckout(
|
|
216
|
+
checkin: string,
|
|
217
|
+
checkout: string,
|
|
218
|
+
): void {
|
|
219
|
+
if (
|
|
220
|
+
Date.parse(`${checkin}T00:00:00Z`) >= Date.parse(`${checkout}T00:00:00Z`)
|
|
221
|
+
) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`--checkin must be earlier than --checkout (got ${checkin} >= ${checkout}).`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function pickCtripCoords(item: CtripSuggestItem): {
|
|
229
|
+
lat: number | null;
|
|
230
|
+
lon: number | null;
|
|
231
|
+
} {
|
|
232
|
+
const candidates: Array<[unknown, unknown]> = [
|
|
233
|
+
[item.gdLat, item.gdLon],
|
|
234
|
+
[item.gLat, item.gLon],
|
|
235
|
+
[item.lat, item.lon],
|
|
236
|
+
];
|
|
237
|
+
for (const [lat, lon] of candidates) {
|
|
238
|
+
const numericLat = Number(lat);
|
|
239
|
+
const numericLon = Number(lon);
|
|
240
|
+
if (
|
|
241
|
+
Number.isFinite(numericLat) &&
|
|
242
|
+
Number.isFinite(numericLon) &&
|
|
243
|
+
(numericLat !== 0 || numericLon !== 0)
|
|
244
|
+
) {
|
|
245
|
+
return { lat: numericLat, lon: numericLon };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { lat: null, lon: null };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function buildCtripSuggestUrl(item: CtripSuggestItem): string | null {
|
|
252
|
+
const id = item.id ? String(item.id) : "";
|
|
253
|
+
const cityId = item.cityId ? String(item.cityId) : "";
|
|
254
|
+
const cityName = item.cityName ? String(item.cityName) : "";
|
|
255
|
+
switch (item.type) {
|
|
256
|
+
case "City":
|
|
257
|
+
return cityId
|
|
258
|
+
? `https://you.ctrip.com/place/${encodeURIComponent(cityName)}${cityId}.html`
|
|
259
|
+
: null;
|
|
260
|
+
case "Markland":
|
|
261
|
+
return id && cityId
|
|
262
|
+
? `https://you.ctrip.com/sight/${encodeURIComponent(cityName)}${cityId}/${id}.html`
|
|
263
|
+
: null;
|
|
264
|
+
case "Hotel":
|
|
265
|
+
return id
|
|
266
|
+
? `https://hotels.ctrip.com/hotels/detail/?hotelid=${id}`
|
|
267
|
+
: null;
|
|
268
|
+
case "BusinessArea":
|
|
269
|
+
case "Zone":
|
|
270
|
+
return cityId && id
|
|
271
|
+
? `https://hotels.ctrip.com/hotels/list?city=${cityId}&zone=${id}`
|
|
272
|
+
: null;
|
|
273
|
+
case "RailwayStation":
|
|
274
|
+
return id ? `https://trains.ctrip.com/trainstation/${id}.html` : null;
|
|
275
|
+
default:
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function mapCtripSuggestRow(
|
|
281
|
+
item: CtripSuggestItem,
|
|
282
|
+
index: number,
|
|
283
|
+
): Record<string, unknown> {
|
|
284
|
+
const { lat, lon } = pickCtripCoords(item);
|
|
285
|
+
return {
|
|
286
|
+
rank: index + 1,
|
|
287
|
+
id: item.id ? String(item.id) : null,
|
|
288
|
+
type: item.type ? String(item.type) : null,
|
|
289
|
+
displayType: nullableString(item.displayType),
|
|
290
|
+
name:
|
|
291
|
+
nullableString(item.displayName) ??
|
|
292
|
+
nullableString(item.word) ??
|
|
293
|
+
nullableString(item.cityName),
|
|
294
|
+
eName: nullableString(item.eName),
|
|
295
|
+
cityId: finiteNonZero(item.cityId),
|
|
296
|
+
cityName: nullableString(item.cityName),
|
|
297
|
+
provinceName: nullableString(item.provinceName),
|
|
298
|
+
countryName: nullableString(item.countryName),
|
|
299
|
+
lat,
|
|
300
|
+
lon,
|
|
301
|
+
score: finiteNonZero(item.commentScore) ?? finiteNonZero(item.cStar),
|
|
302
|
+
url: buildCtripSuggestUrl(item),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function pickCtripHotelMapCoords(mapCoordinate: unknown): {
|
|
307
|
+
lat: number | null;
|
|
308
|
+
lon: number | null;
|
|
309
|
+
} {
|
|
310
|
+
if (!Array.isArray(mapCoordinate) || mapCoordinate.length === 0) {
|
|
311
|
+
return { lat: null, lon: null };
|
|
312
|
+
}
|
|
313
|
+
const ranking = (entry: Record<string, unknown>) => {
|
|
314
|
+
const type = Number(entry.coordinateType);
|
|
315
|
+
if (type === 1) return 0;
|
|
316
|
+
if (type === 2) return 1;
|
|
317
|
+
if (type === 3) return 2;
|
|
318
|
+
return 3;
|
|
319
|
+
};
|
|
320
|
+
const sorted = [...mapCoordinate].sort(
|
|
321
|
+
(a, b) =>
|
|
322
|
+
ranking(a as Record<string, unknown>) -
|
|
323
|
+
ranking(b as Record<string, unknown>),
|
|
324
|
+
);
|
|
325
|
+
for (const entry of sorted) {
|
|
326
|
+
const record = entry as Record<string, unknown>;
|
|
327
|
+
const lat = Number(record.latitude);
|
|
328
|
+
const lon = Number(record.longitude);
|
|
329
|
+
if (
|
|
330
|
+
Number.isFinite(lat) &&
|
|
331
|
+
Number.isFinite(lon) &&
|
|
332
|
+
(lat !== 0 || lon !== 0)
|
|
333
|
+
) {
|
|
334
|
+
return { lat, lon };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return { lat: null, lon: null };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function mapCtripHotelRow(
|
|
341
|
+
entry: CtripHotelEntry,
|
|
342
|
+
index: number,
|
|
343
|
+
): Record<string, unknown> {
|
|
344
|
+
const hotelInfo = entry?.hotelInfo ?? {};
|
|
345
|
+
const rooms = Array.isArray(entry?.roomInfo) ? entry.roomInfo : [];
|
|
346
|
+
const summary = hotelInfo.summary ?? {};
|
|
347
|
+
const nameInfo = hotelInfo.nameInfo ?? {};
|
|
348
|
+
const hotelStar = hotelInfo.hotelStar ?? {};
|
|
349
|
+
const commentInfo = hotelInfo.commentInfo ?? {};
|
|
350
|
+
const positionInfo = hotelInfo.positionInfo ?? {};
|
|
351
|
+
const priceInfo = rooms[0]?.priceInfo ?? {};
|
|
352
|
+
const hotelId = summary.hotelId ? String(summary.hotelId) : null;
|
|
353
|
+
const { lat, lon } = pickCtripHotelMapCoords(positionInfo.mapCoordinate);
|
|
354
|
+
const commenterDigits = commentInfo.commenterNumber
|
|
355
|
+
? String(commentInfo.commenterNumber).replace(/[^\d]/g, "")
|
|
356
|
+
: "";
|
|
357
|
+
const star = finiteNonZero(hotelStar.star);
|
|
358
|
+
const score = finiteNonZero(commentInfo.commentScore);
|
|
359
|
+
const price = finiteNonZero(priceInfo.price);
|
|
360
|
+
return {
|
|
361
|
+
rank: index + 1,
|
|
362
|
+
hotelId,
|
|
363
|
+
name: nullableString(nameInfo.name),
|
|
364
|
+
enName: nullableString(nameInfo.enName),
|
|
365
|
+
star,
|
|
366
|
+
score,
|
|
367
|
+
scoreLabel: nullableString(commentInfo.commentDescription),
|
|
368
|
+
reviewCount: commenterDigits ? Number(commenterDigits) : null,
|
|
369
|
+
cityName: nullableString(positionInfo.cityName),
|
|
370
|
+
district: nullableString(positionInfo.positionDesc),
|
|
371
|
+
address: nullableString(positionInfo.address),
|
|
372
|
+
lat,
|
|
373
|
+
lon,
|
|
374
|
+
price,
|
|
375
|
+
currency: nullableString(priceInfo.currency),
|
|
376
|
+
url: hotelId
|
|
377
|
+
? `https://hotels.ctrip.com/hotels/detail/?hotelid=${hotelId}`
|
|
378
|
+
: null,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function fetchCtripSuggest(
|
|
383
|
+
query: string,
|
|
384
|
+
searchType: "D" | "H",
|
|
385
|
+
): Promise<CtripSuggestItem[]> {
|
|
386
|
+
let response: Response;
|
|
387
|
+
try {
|
|
388
|
+
response = await fetch(SUGGEST_ENDPOINT, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: { "content-type": "application/json" },
|
|
391
|
+
body: JSON.stringify({
|
|
392
|
+
keyword: query,
|
|
393
|
+
searchType,
|
|
394
|
+
platform: "online",
|
|
395
|
+
pageID: "102001",
|
|
396
|
+
head: {
|
|
397
|
+
Locale: "zh-CN",
|
|
398
|
+
LocaleController: "zh_cn",
|
|
399
|
+
Currency: "CNY",
|
|
400
|
+
PageId: "102001",
|
|
401
|
+
clientID: "unicli-ctrip",
|
|
402
|
+
group: "ctrip",
|
|
403
|
+
Frontend: { sessionID: 1, pvid: 1 },
|
|
404
|
+
HotelExtension: { group: "CTRIP", WebpSupport: false },
|
|
405
|
+
},
|
|
406
|
+
}),
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`ctrip suggest fetch failed: ${error instanceof Error ? error.message : String(error)}.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
throw new Error(`ctrip suggest failed with status ${response.status}.`);
|
|
415
|
+
}
|
|
416
|
+
let payload: Record<string, unknown>;
|
|
417
|
+
try {
|
|
418
|
+
payload = (await response.json()) as Record<string, unknown>;
|
|
419
|
+
} catch (error) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`ctrip suggest returned invalid JSON: ${error instanceof Error ? error.message : String(error)}.`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (payload.Result === false) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`ctrip suggest API returned Result=false (ErrorCode=${String(payload.ErrorCode ?? "unknown")}).`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const responsePayload = payload.Response as
|
|
430
|
+
| { searchResults?: unknown }
|
|
431
|
+
| undefined;
|
|
432
|
+
return Array.isArray(responsePayload?.searchResults)
|
|
433
|
+
? (responsePayload.searchResults as CtripSuggestItem[])
|
|
434
|
+
: [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function buildCtripHotelWaitScript(): string {
|
|
438
|
+
return `new Promise((resolve) => {
|
|
439
|
+
const detect = () => {
|
|
440
|
+
if (location.pathname.includes('captcha') || /验证码|verify the human/i.test(document.body?.innerText || '')) return 'captcha';
|
|
441
|
+
const hotels = window.__NEXT_DATA__?.props?.pageProps?.initListData?.hotelList;
|
|
442
|
+
if (Array.isArray(hotels)) return 'content';
|
|
443
|
+
return null;
|
|
444
|
+
};
|
|
445
|
+
const found = detect();
|
|
446
|
+
if (found) return resolve(found);
|
|
447
|
+
const observer = new MutationObserver(() => {
|
|
448
|
+
const result = detect();
|
|
449
|
+
if (result) { observer.disconnect(); resolve(result); }
|
|
450
|
+
});
|
|
451
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
452
|
+
setTimeout(() => { observer.disconnect(); resolve('timeout'); }, 5000);
|
|
453
|
+
})`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function buildCtripHotelExtractScript(): string {
|
|
457
|
+
return `(() => {
|
|
458
|
+
const list = window.__NEXT_DATA__?.props?.pageProps?.initListData?.hotelList;
|
|
459
|
+
return Array.isArray(list) ? list : null;
|
|
460
|
+
})()`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function buildCtripFlightWaitScript(): string {
|
|
464
|
+
return `new Promise((resolve) => {
|
|
465
|
+
const detect = () => {
|
|
466
|
+
if (location.pathname.includes('captcha') || /验证码|verify the human/i.test(document.body?.innerText || '')) return 'captcha';
|
|
467
|
+
if (document.querySelector('.flight-list > span > div')) return 'content';
|
|
468
|
+
return null;
|
|
469
|
+
};
|
|
470
|
+
const found = detect();
|
|
471
|
+
if (found) return resolve(found);
|
|
472
|
+
const observer = new MutationObserver(() => {
|
|
473
|
+
const result = detect();
|
|
474
|
+
if (result) { observer.disconnect(); resolve(result); }
|
|
475
|
+
});
|
|
476
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
477
|
+
setTimeout(() => { observer.disconnect(); resolve('timeout'); }, 8000);
|
|
478
|
+
})`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function buildCtripScrollUntilScript(
|
|
482
|
+
rowSelector: string,
|
|
483
|
+
targetCount: number,
|
|
484
|
+
maxScrolls = 8,
|
|
485
|
+
): string {
|
|
486
|
+
if (!Number.isInteger(targetCount) || targetCount < 1 || targetCount > 100) {
|
|
487
|
+
throw new Error("targetCount must be an integer between 1 and 100.");
|
|
488
|
+
}
|
|
489
|
+
if (!Number.isInteger(maxScrolls) || maxScrolls < 1 || maxScrolls > 30) {
|
|
490
|
+
throw new Error("maxScrolls must be an integer between 1 and 30.");
|
|
491
|
+
}
|
|
492
|
+
return `(async () => {
|
|
493
|
+
const selector = ${JSON.stringify(rowSelector)};
|
|
494
|
+
const isVisible = (element) => {
|
|
495
|
+
const style = window.getComputedStyle(element);
|
|
496
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
|
|
497
|
+
const rect = element.getBoundingClientRect();
|
|
498
|
+
return rect.width > 0 && rect.height > 0;
|
|
499
|
+
};
|
|
500
|
+
const countItems = () => Array.from(document.querySelectorAll(selector)).filter(isVisible).length;
|
|
501
|
+
let lastCount = countItems();
|
|
502
|
+
let plateauRounds = 0;
|
|
503
|
+
for (let index = 0; index < ${maxScrolls}; index += 1) {
|
|
504
|
+
if (countItems() >= ${targetCount}) break;
|
|
505
|
+
const lastHeight = document.body.scrollHeight;
|
|
506
|
+
window.scrollTo(0, lastHeight);
|
|
507
|
+
await new Promise((resolve) => {
|
|
508
|
+
let timeout;
|
|
509
|
+
const observer = new MutationObserver(() => {
|
|
510
|
+
if (document.body.scrollHeight > lastHeight) {
|
|
511
|
+
clearTimeout(timeout);
|
|
512
|
+
observer.disconnect();
|
|
513
|
+
setTimeout(resolve, 200);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
517
|
+
timeout = setTimeout(() => { observer.disconnect(); resolve(null); }, 2500);
|
|
518
|
+
});
|
|
519
|
+
const newCount = countItems();
|
|
520
|
+
if (newCount === lastCount) {
|
|
521
|
+
plateauRounds += 1;
|
|
522
|
+
if (plateauRounds >= 2) break;
|
|
523
|
+
} else {
|
|
524
|
+
plateauRounds = 0;
|
|
525
|
+
lastCount = newCount;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return countItems();
|
|
529
|
+
})()`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function buildCtripFlightExtractScript(): string {
|
|
533
|
+
return `(() => {
|
|
534
|
+
const cleanText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
535
|
+
const isTime = (value) => /^([01]?\\d|2[0-3]):[0-5]\\d$/.test(value);
|
|
536
|
+
const isCurrency = (value) => /^[¥$€£]$/.test(value);
|
|
537
|
+
const isPriceDigits = (value) => /^\\d+([.,]\\d+)?$/.test(value);
|
|
538
|
+
const isFlightNo = (value) => /^[A-Z0-9]{2}\\d{3,4}[A-Z]?$/.test(value);
|
|
539
|
+
const rows = [];
|
|
540
|
+
document.querySelectorAll('.flight-list > span > div').forEach((card) => {
|
|
541
|
+
const chunks = [];
|
|
542
|
+
const walk = (node) => {
|
|
543
|
+
for (const child of node.childNodes) {
|
|
544
|
+
if (child.nodeType === 3) {
|
|
545
|
+
const text = cleanText(child.textContent);
|
|
546
|
+
if (text) chunks.push(text);
|
|
547
|
+
} else if (child.nodeType === 1) {
|
|
548
|
+
walk(child);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
walk(card);
|
|
553
|
+
if (chunks.length < 8) return;
|
|
554
|
+
const firstTimeIndex = chunks.findIndex(isTime);
|
|
555
|
+
if (firstTimeIndex < 1) return;
|
|
556
|
+
const airline = chunks[0];
|
|
557
|
+
const flightNo = chunks[1] || null;
|
|
558
|
+
if (!airline || !isFlightNo(flightNo)) return;
|
|
559
|
+
const aircraft = chunks[2] && !isTime(chunks[2]) ? chunks[2] : null;
|
|
560
|
+
const departureTime = chunks[firstTimeIndex];
|
|
561
|
+
const departureAirport = chunks[firstTimeIndex + 1] || null;
|
|
562
|
+
const arrivalTimeIndex = chunks.findIndex((chunk, index) => index > firstTimeIndex && isTime(chunk));
|
|
563
|
+
if (arrivalTimeIndex < 0) return;
|
|
564
|
+
const arrivalTime = chunks[arrivalTimeIndex];
|
|
565
|
+
const arrivalAirport = chunks[arrivalTimeIndex + 1] || null;
|
|
566
|
+
if (!departureAirport || !arrivalAirport) return;
|
|
567
|
+
const terminal = /^T\\d$/.test(chunks[arrivalTimeIndex + 2] || '') ? chunks[arrivalTimeIndex + 2] : null;
|
|
568
|
+
let price = null;
|
|
569
|
+
let currency = null;
|
|
570
|
+
for (let index = 0; index < chunks.length - 1; index += 1) {
|
|
571
|
+
if (isCurrency(chunks[index]) && isPriceDigits(chunks[index + 1])) {
|
|
572
|
+
currency = chunks[index];
|
|
573
|
+
price = Number(chunks[index + 1].replace(',', ''));
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
let cabin = null;
|
|
578
|
+
for (let index = chunks.length - 1; index >= 0; index -= 1) {
|
|
579
|
+
if (/舱$/.test(chunks[index])) {
|
|
580
|
+
cabin = chunks[index];
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
rows.push({ airline, flightNo, aircraft, departureTime, departureAirport, arrivalTime, arrivalAirport, terminal, price, currency, cabin });
|
|
585
|
+
});
|
|
586
|
+
return rows;
|
|
587
|
+
})()`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function runSuggestCommand(
|
|
591
|
+
kwargs: Record<string, unknown>,
|
|
592
|
+
searchType: "D" | "H",
|
|
593
|
+
emptyLabel: string,
|
|
594
|
+
): Promise<Record<string, unknown>[]> {
|
|
595
|
+
const query = requireCtripQuery(kwargs.query);
|
|
596
|
+
const limit = parseCtripLimit(kwargs.limit);
|
|
597
|
+
const raw = await fetchCtripSuggest(query, searchType);
|
|
598
|
+
const rows = raw
|
|
599
|
+
.filter((item) => Boolean(item) && typeof item === "object")
|
|
600
|
+
.slice(0, limit)
|
|
601
|
+
.map(mapCtripSuggestRow)
|
|
602
|
+
.filter((row) => row.name);
|
|
603
|
+
if (!rows.length) throw new Error(`${emptyLabel} returned no data.`);
|
|
604
|
+
return rows;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
cli({
|
|
608
|
+
site: "ctrip",
|
|
609
|
+
name: "search",
|
|
610
|
+
description:
|
|
611
|
+
"Search Ctrip destinations, landmarks, scenic spots, and stations",
|
|
612
|
+
strategy: Strategy.PUBLIC,
|
|
613
|
+
browser: false,
|
|
614
|
+
args: [
|
|
615
|
+
{ name: "query", type: "str", required: true, positional: true },
|
|
616
|
+
{ name: "limit", type: "int", default: SUGGEST_DEFAULT_LIMIT },
|
|
617
|
+
],
|
|
618
|
+
columns: CTRIP_SUGGEST_COLUMNS,
|
|
619
|
+
func: async (_page, kwargs) => runSuggestCommand(kwargs, "D", "ctrip search"),
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
cli({
|
|
623
|
+
site: "ctrip",
|
|
624
|
+
name: "hotel-suggest",
|
|
625
|
+
description: "Search Ctrip hotel context suggestions",
|
|
626
|
+
strategy: Strategy.PUBLIC,
|
|
627
|
+
browser: false,
|
|
628
|
+
args: [
|
|
629
|
+
{ name: "query", type: "str", required: true, positional: true },
|
|
630
|
+
{ name: "limit", type: "int", default: SUGGEST_DEFAULT_LIMIT },
|
|
631
|
+
],
|
|
632
|
+
columns: CTRIP_SUGGEST_COLUMNS,
|
|
633
|
+
func: async (_page, kwargs) =>
|
|
634
|
+
runSuggestCommand(kwargs, "H", "ctrip hotel-suggest"),
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
cli({
|
|
638
|
+
site: "ctrip",
|
|
639
|
+
name: "hotel-search",
|
|
640
|
+
description: "Search Ctrip hotel list by city and stay dates",
|
|
641
|
+
domain: "hotels.ctrip.com",
|
|
642
|
+
strategy: Strategy.COOKIE,
|
|
643
|
+
browser: true,
|
|
644
|
+
args: [
|
|
645
|
+
{ name: "city", type: "int", required: true, positional: true },
|
|
646
|
+
{ name: "checkin", type: "str", required: true },
|
|
647
|
+
{ name: "checkout", type: "str", required: true },
|
|
648
|
+
{ name: "limit", type: "int", default: HOTEL_DEFAULT_LIMIT },
|
|
649
|
+
],
|
|
650
|
+
columns: HOTEL_COLUMNS,
|
|
651
|
+
func: async (page, kwargs) => {
|
|
652
|
+
const p = page as IPage;
|
|
653
|
+
const cityId = parseCtripCityId(kwargs.city);
|
|
654
|
+
const checkin = parseCtripIsoDate("checkin", kwargs.checkin);
|
|
655
|
+
const checkout = parseCtripIsoDate("checkout", kwargs.checkout);
|
|
656
|
+
assertCtripCheckinBeforeCheckout(checkin, checkout);
|
|
657
|
+
const limit = parseCtripLimit(
|
|
658
|
+
kwargs.limit,
|
|
659
|
+
HOTEL_DEFAULT_LIMIT,
|
|
660
|
+
HOTEL_MAX_LIMIT,
|
|
661
|
+
);
|
|
662
|
+
const url = `https://hotels.ctrip.com/hotels/list?city=${cityId}&checkin=${checkin}&checkout=${checkout}`;
|
|
663
|
+
await p.goto(url);
|
|
664
|
+
const waitResult = await p.evaluate(buildCtripHotelWaitScript());
|
|
665
|
+
if (waitResult === "captcha") {
|
|
666
|
+
throw new Error(
|
|
667
|
+
"Ctrip is asking for a captcha; complete it in the browser session and retry.",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (waitResult !== "content") {
|
|
671
|
+
throw new Error(
|
|
672
|
+
`Ctrip hotel-search page did not expose SSR hotel list (state=${String(waitResult)}).`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const raw = await p.evaluate(buildCtripHotelExtractScript());
|
|
676
|
+
if (!Array.isArray(raw)) {
|
|
677
|
+
throw new Error("Ctrip hotel-search returned malformed SSR hotel list.");
|
|
678
|
+
}
|
|
679
|
+
if (raw.length === 0) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`No hotels for city=${cityId} on ${checkin} to ${checkout}.`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const rows = raw
|
|
685
|
+
.map((entry, index) => mapCtripHotelRow(entry as CtripHotelEntry, index))
|
|
686
|
+
.filter((row) => row.hotelId && row.name)
|
|
687
|
+
.slice(0, limit);
|
|
688
|
+
if (!rows.length) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
"Ctrip hotel-search SSR rows were missing required hotelId/name anchors.",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return rows;
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
cli({
|
|
698
|
+
site: "ctrip",
|
|
699
|
+
name: "flight",
|
|
700
|
+
description: "Search Ctrip one-way flights by IATA route and date",
|
|
701
|
+
domain: "flights.ctrip.com",
|
|
702
|
+
strategy: Strategy.COOKIE,
|
|
703
|
+
browser: true,
|
|
704
|
+
args: [
|
|
705
|
+
{ name: "from", type: "str", required: true, positional: true },
|
|
706
|
+
{ name: "to", type: "str", required: true, positional: true },
|
|
707
|
+
{ name: "date", type: "str", required: true },
|
|
708
|
+
{ name: "limit", type: "int", default: FLIGHT_DEFAULT_LIMIT },
|
|
709
|
+
],
|
|
710
|
+
columns: FLIGHT_COLUMNS,
|
|
711
|
+
func: async (page, kwargs) => {
|
|
712
|
+
const p = page as IPage;
|
|
713
|
+
const fromCode = parseCtripIataCode("from", kwargs.from);
|
|
714
|
+
const toCode = parseCtripIataCode("to", kwargs.to);
|
|
715
|
+
if (fromCode === toCode) {
|
|
716
|
+
throw new Error(`--from and --to must differ (got ${fromCode}).`);
|
|
717
|
+
}
|
|
718
|
+
const date = parseCtripIsoDate("date", kwargs.date);
|
|
719
|
+
const limit = parseCtripLimit(
|
|
720
|
+
kwargs.limit,
|
|
721
|
+
FLIGHT_DEFAULT_LIMIT,
|
|
722
|
+
FLIGHT_MAX_LIMIT,
|
|
723
|
+
);
|
|
724
|
+
const url =
|
|
725
|
+
`https://flights.ctrip.com/online/list/oneway-${fromCode.toLowerCase()}-${toCode.toLowerCase()}` +
|
|
726
|
+
`?depdate=${date}&cabin=Y_S_C_F&adult=1&child=0&infant=0`;
|
|
727
|
+
await p.goto(url);
|
|
728
|
+
const waitResult = await p.evaluate(buildCtripFlightWaitScript());
|
|
729
|
+
if (waitResult === "captcha") {
|
|
730
|
+
throw new Error(
|
|
731
|
+
"Ctrip is asking for a captcha; complete it in the browser session and retry.",
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (waitResult !== "content") {
|
|
735
|
+
throw new Error(
|
|
736
|
+
`Ctrip flight page did not render flight cards (state=${String(waitResult)}).`,
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
const renderedCardCount = await p.evaluate(
|
|
740
|
+
buildCtripScrollUntilScript(".flight-list > span > div", limit),
|
|
741
|
+
);
|
|
742
|
+
const raw = await p.evaluate(buildCtripFlightExtractScript());
|
|
743
|
+
if (!Array.isArray(raw)) {
|
|
744
|
+
throw new Error("Ctrip flight DOM extraction returned malformed rows.");
|
|
745
|
+
}
|
|
746
|
+
if (raw.length === 0) {
|
|
747
|
+
if (Number(renderedCardCount) > 0) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
"Ctrip flight cards rendered but parser did not find required flight anchors.",
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
throw new Error(`No flights for ${fromCode} to ${toCode} on ${date}.`);
|
|
753
|
+
}
|
|
754
|
+
const rows = raw
|
|
755
|
+
.filter((row) => {
|
|
756
|
+
const flight = row as CtripFlightRow;
|
|
757
|
+
return (
|
|
758
|
+
flight.departureTime &&
|
|
759
|
+
flight.departureAirport &&
|
|
760
|
+
flight.arrivalTime &&
|
|
761
|
+
flight.arrivalAirport &&
|
|
762
|
+
flight.airline &&
|
|
763
|
+
flight.flightNo
|
|
764
|
+
);
|
|
765
|
+
})
|
|
766
|
+
.slice(0, limit)
|
|
767
|
+
.map((row, index) => {
|
|
768
|
+
const flight = row as CtripFlightRow;
|
|
769
|
+
return {
|
|
770
|
+
rank: index + 1,
|
|
771
|
+
airline: flight.airline,
|
|
772
|
+
flightNo: flight.flightNo,
|
|
773
|
+
aircraft: flight.aircraft,
|
|
774
|
+
departureTime: flight.departureTime,
|
|
775
|
+
departureAirport: flight.departureAirport,
|
|
776
|
+
arrivalTime: flight.arrivalTime,
|
|
777
|
+
arrivalAirport: flight.arrivalAirport,
|
|
778
|
+
terminal: flight.terminal,
|
|
779
|
+
price: flight.price,
|
|
780
|
+
currency: flight.currency,
|
|
781
|
+
cabin: flight.cabin,
|
|
782
|
+
url,
|
|
783
|
+
};
|
|
784
|
+
});
|
|
785
|
+
if (!rows.length) {
|
|
786
|
+
throw new Error(
|
|
787
|
+
"Ctrip flight rows were missing required airline/flight/time/airport anchors.",
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return rows;
|
|
791
|
+
},
|
|
792
|
+
});
|