@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,758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owner src/adapters/qwen/web.ts
|
|
3
|
+
* @does Register agent-facing Qwen web chat commands implemented with site-specific safety checks.
|
|
4
|
+
* @needs Logged-in qianwen.com browser session, Qwen chat DOM/API, optional local image output directory.
|
|
5
|
+
* @feeds surface coverage ledger and Qwen ask/read/send/history/detail/new/status/image workflows.
|
|
6
|
+
* @breaks Qwen DOM/API drift or weak session-id validation can miss or target the wrong chat.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { cli, Strategy } from "../../registry.js";
|
|
13
|
+
import { boolArg, js, str } from "../_shared/browser-tools.js";
|
|
14
|
+
|
|
15
|
+
const QWEN_HOME = "https://www.qianwen.com/";
|
|
16
|
+
const QWEN_API_DOMAIN = "chat2-api.qianwen.com";
|
|
17
|
+
const SESSION_ID_RE = /^[a-f0-9]{32}$/i;
|
|
18
|
+
const VISIBLE_JS = `const isVisible = (node) => {
|
|
19
|
+
if (!(node instanceof Element)) return false;
|
|
20
|
+
const style = window.getComputedStyle(node);
|
|
21
|
+
if (style.visibility === 'hidden' || style.display === 'none') return false;
|
|
22
|
+
const rect = node.getBoundingClientRect();
|
|
23
|
+
return rect.width > 0 && rect.height > 0;
|
|
24
|
+
};`;
|
|
25
|
+
|
|
26
|
+
interface QwenBrowserPage {
|
|
27
|
+
goto: (url: string, opts?: Record<string, unknown>) => Promise<unknown>;
|
|
28
|
+
wait: (args: unknown) => Promise<unknown>;
|
|
29
|
+
evaluate: (script: string) => Promise<unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface QwenBubble {
|
|
33
|
+
id?: unknown;
|
|
34
|
+
role?: unknown;
|
|
35
|
+
text?: unknown;
|
|
36
|
+
html?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface QwenSession {
|
|
40
|
+
id?: unknown;
|
|
41
|
+
title?: unknown;
|
|
42
|
+
updated_at?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseQwenSessionId(value: unknown): string {
|
|
46
|
+
const raw = str(value).trim();
|
|
47
|
+
if (!raw) throw new Error("Qwen session ID cannot be empty.");
|
|
48
|
+
const urlMatch = raw.match(/qianwen\.com\/chat\/([a-f0-9]{32})(?:[/?#]|$)/i);
|
|
49
|
+
const candidate = urlMatch?.[1] ?? raw;
|
|
50
|
+
if (!SESSION_ID_RE.test(candidate)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Invalid Qwen session ID: ${raw}. Expected a 32-character hex ID or https://www.qianwen.com/chat/<id>.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return candidate.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function qwenHtmlToMarkdown(value: unknown): string {
|
|
59
|
+
return str(value)
|
|
60
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
61
|
+
.replace(/<\/(p|div|li|h[1-6]|pre)>/gi, "\n")
|
|
62
|
+
.replace(/<[^>]+>/g, " ")
|
|
63
|
+
.replace(/ /g, " ")
|
|
64
|
+
.replace(/</g, "<")
|
|
65
|
+
.replace(/>/g, ">")
|
|
66
|
+
.replace(/&/g, "&")
|
|
67
|
+
.replace(/"/g, '"')
|
|
68
|
+
.replace(/'/g, "'")
|
|
69
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
70
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
71
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
72
|
+
.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizeQwenBoolean(
|
|
76
|
+
value: unknown,
|
|
77
|
+
fallback = false,
|
|
78
|
+
): boolean {
|
|
79
|
+
if (typeof value === "boolean") return value;
|
|
80
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
81
|
+
return /^(true|1|yes|on)$/i.test(str(value).trim());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function mapQwenBubbles(
|
|
85
|
+
bubbles: QwenBubble[],
|
|
86
|
+
wantMarkdown: boolean,
|
|
87
|
+
): Record<string, unknown>[] {
|
|
88
|
+
return bubbles
|
|
89
|
+
.map((bubble) => {
|
|
90
|
+
const role = str(bubble.role) === "Assistant" ? "Assistant" : "User";
|
|
91
|
+
const text =
|
|
92
|
+
wantMarkdown && role === "Assistant" && str(bubble.html).trim()
|
|
93
|
+
? qwenHtmlToMarkdown(bubble.html) || str(bubble.text)
|
|
94
|
+
: str(bubble.text);
|
|
95
|
+
return { Role: role, Text: text.trim() };
|
|
96
|
+
})
|
|
97
|
+
.filter((row) => row.Text);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatQwenDate(value: unknown): string {
|
|
101
|
+
const n = Number(value);
|
|
102
|
+
if (!Number.isFinite(n) || n <= 0) return "";
|
|
103
|
+
const date = new Date(n);
|
|
104
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
105
|
+
const pad = (part: number) => String(part).padStart(2, "0");
|
|
106
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function mapQwenSessions(
|
|
110
|
+
sessions: QwenSession[],
|
|
111
|
+
): Record<string, unknown>[] {
|
|
112
|
+
return sessions
|
|
113
|
+
.map((session, index) => {
|
|
114
|
+
const id = str(session.id);
|
|
115
|
+
return {
|
|
116
|
+
Index: index + 1,
|
|
117
|
+
Title: str(session.title, "(untitled)").trim() || "(untitled)",
|
|
118
|
+
Updated: formatQwenDate(session.updated_at),
|
|
119
|
+
Url: `${QWEN_HOME}chat/${id}`,
|
|
120
|
+
};
|
|
121
|
+
})
|
|
122
|
+
.filter((row) => /\/chat\/[a-f0-9]{32}$/i.test(str(row.Url)));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function qwenOutputDir(value: unknown): string {
|
|
126
|
+
return str(value, "~/Pictures/qianwen").replace(/^~(?=$|\/)/, homedir());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function qwenBoundedInt(
|
|
130
|
+
value: unknown,
|
|
131
|
+
fallback: number,
|
|
132
|
+
max: number,
|
|
133
|
+
label: string,
|
|
134
|
+
): number {
|
|
135
|
+
const raw = value ?? fallback;
|
|
136
|
+
const n = Number(raw);
|
|
137
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
138
|
+
throw new Error(`Qwen ${label} must be a positive integer.`);
|
|
139
|
+
}
|
|
140
|
+
if (n > max) {
|
|
141
|
+
throw new Error(`Qwen ${label} must be <= ${max}.`);
|
|
142
|
+
}
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function qwenImageExt(mime: unknown): string {
|
|
147
|
+
const value = str(mime).toLowerCase();
|
|
148
|
+
if (value.includes("png")) return ".png";
|
|
149
|
+
if (value.includes("webp")) return ".webp";
|
|
150
|
+
if (value.includes("gif")) return ".gif";
|
|
151
|
+
return ".jpg";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function displayPath(filePath: string): string {
|
|
155
|
+
return filePath.startsWith(homedir())
|
|
156
|
+
? `~${filePath.slice(homedir().length)}`
|
|
157
|
+
: filePath;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function currentUrl(page: QwenBrowserPage): Promise<string> {
|
|
161
|
+
const value = await page.evaluate("window.location.href").catch(() => "");
|
|
162
|
+
return typeof value === "string" ? value : "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function ensureOnQwen(page: QwenBrowserPage): Promise<void> {
|
|
166
|
+
const url = await currentUrl(page);
|
|
167
|
+
if (!url.includes("qianwen.com")) {
|
|
168
|
+
await page.goto(QWEN_HOME, { waitUntil: "load", settleMs: 2500 });
|
|
169
|
+
await page.wait(2);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function dismissLoginModal(page: QwenBrowserPage): Promise<void> {
|
|
174
|
+
await page.evaluate(`(() => {
|
|
175
|
+
${VISIBLE_JS}
|
|
176
|
+
const modal = document.querySelector('[role=alert-biz-modal]');
|
|
177
|
+
if (!modal || !isVisible(modal)) return false;
|
|
178
|
+
const target = Array.from(modal.querySelectorAll('div, button, span'))
|
|
179
|
+
.filter((node) => node instanceof HTMLElement && isVisible(node))
|
|
180
|
+
.find((node) => /close|dismiss|cancel/i.test(String(node.className || '')) || node.getAttribute('aria-label') === '关闭')
|
|
181
|
+
|| modal.querySelector('svg')?.parentElement;
|
|
182
|
+
if (target instanceof HTMLElement) {
|
|
183
|
+
target.click();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true }));
|
|
187
|
+
return true;
|
|
188
|
+
})()`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function hasLoginGate(page: QwenBrowserPage): Promise<boolean> {
|
|
192
|
+
const result = await page.evaluate(`(() => {
|
|
193
|
+
${VISIBLE_JS}
|
|
194
|
+
const modal = document.querySelector('[role=alert-biz-modal]');
|
|
195
|
+
if (modal && isVisible(modal)) {
|
|
196
|
+
const src = modal.querySelector('iframe')?.getAttribute('src') || '';
|
|
197
|
+
if (src.includes('passport.qianwen.com') || src.includes('login')) return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
})()`);
|
|
201
|
+
return Boolean(result);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function isLoggedIn(page: QwenBrowserPage): Promise<boolean> {
|
|
205
|
+
const result = await page.evaluate(`(() => {
|
|
206
|
+
${VISIBLE_JS}
|
|
207
|
+
const loginButton = Array.from(document.querySelectorAll('button'))
|
|
208
|
+
.find((node) => (node.textContent || '').trim() === '登录' && isVisible(node));
|
|
209
|
+
if (loginButton) return false;
|
|
210
|
+
const hint = Array.from(document.querySelectorAll('p'))
|
|
211
|
+
.find((node) => (node.textContent || '').includes('登录可同步历史对话'));
|
|
212
|
+
return !(hint && isVisible(hint));
|
|
213
|
+
})()`);
|
|
214
|
+
return Boolean(result);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function startNewChat(page: QwenBrowserPage): Promise<void> {
|
|
218
|
+
const result = await page.evaluate(`(() => {
|
|
219
|
+
${VISIBLE_JS}
|
|
220
|
+
const button = Array.from(document.querySelectorAll('button'))
|
|
221
|
+
.find((node) => isVisible(node) && (node.innerText || '').trim() === '新建对话');
|
|
222
|
+
if (button instanceof HTMLElement) {
|
|
223
|
+
button.click();
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
})()`);
|
|
228
|
+
if (result) {
|
|
229
|
+
await page.wait(1.5);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
await page.goto(QWEN_HOME, { waitUntil: "load", settleMs: 2500 });
|
|
233
|
+
await page.wait(2);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function setFeatureToggle(
|
|
237
|
+
page: QwenBrowserPage,
|
|
238
|
+
feature: "think" | "research" | "image",
|
|
239
|
+
enabled: boolean,
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
const labels: Record<typeof feature, string> = {
|
|
242
|
+
think: "深度思考",
|
|
243
|
+
research: "深度研究",
|
|
244
|
+
image: "AI生图",
|
|
245
|
+
};
|
|
246
|
+
await page.evaluate(`(async () => {
|
|
247
|
+
${VISIBLE_JS}
|
|
248
|
+
const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
249
|
+
const button = Array.from(document.querySelectorAll('button[aria-label]'))
|
|
250
|
+
.find((node) => isVisible(node) && node.getAttribute('aria-label') === ${js(labels[feature])});
|
|
251
|
+
if (!(button instanceof HTMLElement)) return false;
|
|
252
|
+
const selected = button.getAttribute('aria-pressed') === 'true'
|
|
253
|
+
|| /active|selected|bg-primary/.test(String(button.className || ''));
|
|
254
|
+
if (selected === ${JSON.stringify(enabled)}) return true;
|
|
255
|
+
button.click();
|
|
256
|
+
await waitFor(300);
|
|
257
|
+
return true;
|
|
258
|
+
})()`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function sendQwenMessage(
|
|
262
|
+
page: QwenBrowserPage,
|
|
263
|
+
prompt: string,
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
const result = (await page.evaluate(`(async () => {
|
|
266
|
+
${VISIBLE_JS}
|
|
267
|
+
const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
268
|
+
const editor = Array.from(document.querySelectorAll('[role=textbox][contenteditable=true]'))
|
|
269
|
+
.find((node) => isVisible(node));
|
|
270
|
+
if (!(editor instanceof HTMLElement)) return { ok: false, reason: 'Qwen composer not found.' };
|
|
271
|
+
editor.focus();
|
|
272
|
+
const selection = window.getSelection();
|
|
273
|
+
const range = document.createRange();
|
|
274
|
+
range.selectNodeContents(editor);
|
|
275
|
+
selection?.removeAllRanges();
|
|
276
|
+
selection?.addRange(range);
|
|
277
|
+
document.execCommand('delete', false);
|
|
278
|
+
await waitFor(100);
|
|
279
|
+
editor.dispatchEvent(new InputEvent('beforeinput', {
|
|
280
|
+
inputType: 'insertText',
|
|
281
|
+
data: ${js(prompt)},
|
|
282
|
+
bubbles: true,
|
|
283
|
+
cancelable: true,
|
|
284
|
+
}));
|
|
285
|
+
await waitFor(400);
|
|
286
|
+
const sendButton = document.querySelector('button[aria-label="发送消息"]');
|
|
287
|
+
if (sendButton instanceof HTMLButtonElement && !sendButton.disabled) {
|
|
288
|
+
sendButton.click();
|
|
289
|
+
return { ok: true, action: 'click' };
|
|
290
|
+
}
|
|
291
|
+
editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
292
|
+
editor.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
293
|
+
return { ok: true, action: 'enter' };
|
|
294
|
+
})()`)) as { ok?: boolean; reason?: string };
|
|
295
|
+
if (!result?.ok)
|
|
296
|
+
throw new Error(result?.reason || "Failed to send Qwen prompt.");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function getMessageBubbles(page: QwenBrowserPage): Promise<QwenBubble[]> {
|
|
300
|
+
const result = await page.evaluate(`(() => {
|
|
301
|
+
${VISIBLE_JS}
|
|
302
|
+
const wraps = Array.from(document.querySelectorAll('[data-chat-question-wrap], [data-chat-answers-wrap]'))
|
|
303
|
+
.filter((node) => node instanceof HTMLElement && isVisible(node));
|
|
304
|
+
const findTurnId = (node) => {
|
|
305
|
+
let parent = node.parentElement;
|
|
306
|
+
while (parent && parent !== document.body) {
|
|
307
|
+
const reqEl = parent.querySelector('[data-req-id]');
|
|
308
|
+
if (reqEl?.getAttribute('data-req-id')) return reqEl.getAttribute('data-req-id');
|
|
309
|
+
parent = parent.parentElement;
|
|
310
|
+
}
|
|
311
|
+
return '';
|
|
312
|
+
};
|
|
313
|
+
return wraps.map((node, index) => {
|
|
314
|
+
const isAnswer = node.hasAttribute('data-chat-answers-wrap');
|
|
315
|
+
const contentNode = isAnswer
|
|
316
|
+
? (node.querySelector('#qk-markdown-react') || node.querySelector('[class*="markdown"]') || node)
|
|
317
|
+
: node;
|
|
318
|
+
const html = contentNode instanceof HTMLElement ? (contentNode.innerHTML || '') : '';
|
|
319
|
+
const text = contentNode instanceof HTMLElement ? (contentNode.innerText || contentNode.textContent || '') : '';
|
|
320
|
+
const baseId = findTurnId(node) || ('pos-' + index);
|
|
321
|
+
return {
|
|
322
|
+
id: baseId + (isAnswer ? '-answer' : '-question'),
|
|
323
|
+
role: isAnswer ? 'Assistant' : 'User',
|
|
324
|
+
text: String(text || '').replace(/\\s+/g, ' ').trim(),
|
|
325
|
+
html,
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
})()`);
|
|
329
|
+
return Array.isArray(result)
|
|
330
|
+
? (result as QwenBubble[]).filter(
|
|
331
|
+
(bubble) => str(bubble.id) && str(bubble.text),
|
|
332
|
+
)
|
|
333
|
+
: [];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function waitForQwenAnswer(
|
|
337
|
+
page: QwenBrowserPage,
|
|
338
|
+
prompt: string,
|
|
339
|
+
timeout: number,
|
|
340
|
+
): Promise<QwenBubble> {
|
|
341
|
+
const startedAt = Date.now();
|
|
342
|
+
let previous = "";
|
|
343
|
+
let stable = 0;
|
|
344
|
+
let candidate: QwenBubble | null = null;
|
|
345
|
+
while (Date.now() - startedAt < timeout * 1000) {
|
|
346
|
+
await page.wait(2);
|
|
347
|
+
if (await hasLoginGate(page)) throw new Error("Qwen login is required.");
|
|
348
|
+
const bubbles = await getMessageBubbles(page);
|
|
349
|
+
const assistant = [...bubbles]
|
|
350
|
+
.reverse()
|
|
351
|
+
.find((bubble) => bubble.role === "Assistant");
|
|
352
|
+
const text = str(assistant?.text).trim();
|
|
353
|
+
if (!text || text === prompt.trim()) continue;
|
|
354
|
+
candidate = assistant ?? null;
|
|
355
|
+
if (text === previous) {
|
|
356
|
+
stable += 1;
|
|
357
|
+
if (Date.now() - startedAt >= 6000 && stable >= 2 && candidate) {
|
|
358
|
+
return candidate;
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
previous = text;
|
|
362
|
+
stable = 0;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (candidate) return candidate;
|
|
366
|
+
throw new Error("No Qwen reply observed before timeout.");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function getSessionList(
|
|
370
|
+
page: QwenBrowserPage,
|
|
371
|
+
limit: number,
|
|
372
|
+
): Promise<Record<string, unknown>[]> {
|
|
373
|
+
const raw = (await page.evaluate(`(async () => {
|
|
374
|
+
try {
|
|
375
|
+
const utdid = (document.cookie.match(/(?:^|;\\s*)b-user-id=([^;]+)/)?.[1])
|
|
376
|
+
|| (document.cookie.match(/(?:^|;\\s*)utdid=([^;]+)/)?.[1])
|
|
377
|
+
|| '';
|
|
378
|
+
const query = new URLSearchParams({
|
|
379
|
+
biz_id: 'ai_qwen',
|
|
380
|
+
chat_client: 'h5',
|
|
381
|
+
device: 'pc',
|
|
382
|
+
fr: 'pc',
|
|
383
|
+
pr: 'qwen',
|
|
384
|
+
ut: utdid,
|
|
385
|
+
la: 'zh-CN',
|
|
386
|
+
tz: 'Asia/Shanghai',
|
|
387
|
+
ve: '2.4.9',
|
|
388
|
+
}).toString();
|
|
389
|
+
const response = await fetch('https://${QWEN_API_DOMAIN}/api/v2/session/page/list?' + query, {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
credentials: 'include',
|
|
392
|
+
headers: { 'Content-Type': 'application/json' },
|
|
393
|
+
body: JSON.stringify({ page_num: 1, page_size: ${limit}, page_no: 1 }),
|
|
394
|
+
});
|
|
395
|
+
const text = await response.text();
|
|
396
|
+
let body = null;
|
|
397
|
+
try { body = text ? JSON.parse(text) : null; } catch { body = null; }
|
|
398
|
+
return { ok: response.ok, status: response.status, body };
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return { ok: false, status: 0, error: String(error?.message || error) };
|
|
401
|
+
}
|
|
402
|
+
})()`)) as { ok?: boolean; status?: number; body?: Record<string, unknown> };
|
|
403
|
+
if (!raw?.ok)
|
|
404
|
+
throw new Error(`Qwen history API failed (status=${raw?.status ?? 0}).`);
|
|
405
|
+
const data = (raw.body?.data || raw.body?.result || {}) as Record<
|
|
406
|
+
string,
|
|
407
|
+
unknown
|
|
408
|
+
>;
|
|
409
|
+
const list = Array.isArray(data.list)
|
|
410
|
+
? data.list
|
|
411
|
+
: Array.isArray(data.items)
|
|
412
|
+
? data.items
|
|
413
|
+
: Array.isArray(data.page_list)
|
|
414
|
+
? data.page_list
|
|
415
|
+
: Array.isArray(raw.body?.list)
|
|
416
|
+
? raw.body.list
|
|
417
|
+
: [];
|
|
418
|
+
const sessions = list
|
|
419
|
+
.map((item) => {
|
|
420
|
+
const row = item as Record<string, unknown>;
|
|
421
|
+
return {
|
|
422
|
+
id: str(row.session_id || row.sessionId || row.id),
|
|
423
|
+
title: str(row.title || row.name || row.summary).trim(),
|
|
424
|
+
updated_at: Number(
|
|
425
|
+
row.updated_at ||
|
|
426
|
+
row.last_req_timestamp ||
|
|
427
|
+
row.updatedAt ||
|
|
428
|
+
row.gmt_modified ||
|
|
429
|
+
row.update_time ||
|
|
430
|
+
0,
|
|
431
|
+
),
|
|
432
|
+
};
|
|
433
|
+
})
|
|
434
|
+
.filter((item) => item.id);
|
|
435
|
+
const rows = mapQwenSessions(sessions).slice(0, limit);
|
|
436
|
+
if (!rows.length) throw new Error("No Qwen conversations found.");
|
|
437
|
+
return rows;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function collectImageUrls(
|
|
441
|
+
page: QwenBrowserPage,
|
|
442
|
+
assistantId: string,
|
|
443
|
+
): Promise<string[]> {
|
|
444
|
+
const urls = await page.evaluate(`(() => {
|
|
445
|
+
const scope = ${js(assistantId)};
|
|
446
|
+
const bubbles = Array.from(document.querySelectorAll('[data-msgid$="-answer"], [data-chat-answers-wrap]'));
|
|
447
|
+
const target = scope
|
|
448
|
+
? bubbles.find((node) => node.getAttribute('data-msgid') === scope)
|
|
449
|
+
: bubbles[bubbles.length - 1];
|
|
450
|
+
if (!target) return [];
|
|
451
|
+
const imgs = Array.from(target.querySelectorAll('img'))
|
|
452
|
+
.map((node) => node.getAttribute('src') || '')
|
|
453
|
+
.filter((src) => src && !src.startsWith('data:') && !/\\.(svg)$/i.test(src) && !src.includes('alicdn.com/imgextra'));
|
|
454
|
+
return Array.from(new Set(imgs));
|
|
455
|
+
})()`);
|
|
456
|
+
return Array.isArray(urls) ? urls.map(String) : [];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function waitForImageUrls(
|
|
460
|
+
page: QwenBrowserPage,
|
|
461
|
+
assistantId: string,
|
|
462
|
+
timeout: number,
|
|
463
|
+
): Promise<string[]> {
|
|
464
|
+
const startedAt = Date.now();
|
|
465
|
+
let lastUrls: string[] = [];
|
|
466
|
+
while (Date.now() - startedAt < timeout * 1000) {
|
|
467
|
+
await page.wait(2);
|
|
468
|
+
if (await hasLoginGate(page)) throw new Error("Qwen login is required.");
|
|
469
|
+
const urls = await collectImageUrls(page, assistantId);
|
|
470
|
+
if (
|
|
471
|
+
urls.length &&
|
|
472
|
+
urls.length === lastUrls.length &&
|
|
473
|
+
urls.every((url, i) => url === lastUrls[i])
|
|
474
|
+
) {
|
|
475
|
+
return urls;
|
|
476
|
+
}
|
|
477
|
+
lastUrls = urls;
|
|
478
|
+
}
|
|
479
|
+
if (lastUrls.length) return lastUrls;
|
|
480
|
+
throw new Error("No generated Qwen images observed before timeout.");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function fetchImageAsset(
|
|
484
|
+
page: QwenBrowserPage,
|
|
485
|
+
url: string,
|
|
486
|
+
): Promise<{ mime: string; base64: string }> {
|
|
487
|
+
const result = (await page.evaluate(`(async () => {
|
|
488
|
+
const response = await fetch(${js(url)}, { credentials: 'include' });
|
|
489
|
+
if (!response.ok) return { ok: false, status: response.status };
|
|
490
|
+
const mime = response.headers.get('content-type') || '';
|
|
491
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
492
|
+
let binary = '';
|
|
493
|
+
const chunk = 0x8000;
|
|
494
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
495
|
+
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
|
496
|
+
}
|
|
497
|
+
return { ok: true, mime, base64: btoa(binary) };
|
|
498
|
+
})()`)) as { ok?: boolean; status?: number; mime?: string; base64?: string };
|
|
499
|
+
if (!result?.ok || !result.base64) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`Failed to fetch generated Qwen image: status=${result?.status ?? "?"}.`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
return { mime: str(result.mime), base64: result.base64 };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
cli({
|
|
508
|
+
site: "qwen",
|
|
509
|
+
name: "ask",
|
|
510
|
+
description: "Send a prompt to Qwen and return the assistant reply",
|
|
511
|
+
domain: "www.qianwen.com",
|
|
512
|
+
strategy: Strategy.COOKIE,
|
|
513
|
+
browser: true,
|
|
514
|
+
args: [
|
|
515
|
+
{ name: "prompt", type: "str", required: true, positional: true },
|
|
516
|
+
{ name: "timeout", type: "int", default: 120 },
|
|
517
|
+
{ name: "new", type: "bool", default: false },
|
|
518
|
+
{ name: "think", type: "bool", default: false },
|
|
519
|
+
{ name: "research", type: "bool", default: false },
|
|
520
|
+
{ name: "markdown", type: "bool", default: false },
|
|
521
|
+
],
|
|
522
|
+
columns: ["Role", "Text"],
|
|
523
|
+
func: async (page, kwargs) => {
|
|
524
|
+
const prompt = str(kwargs.prompt).trim();
|
|
525
|
+
if (!prompt) throw new Error("Qwen prompt cannot be empty.");
|
|
526
|
+
const p = page as QwenBrowserPage;
|
|
527
|
+
const timeout = qwenBoundedInt(kwargs.timeout, 120, 600, "timeout");
|
|
528
|
+
await ensureOnQwen(p);
|
|
529
|
+
await dismissLoginModal(p);
|
|
530
|
+
if (boolArg(kwargs.new)) await startNewChat(p);
|
|
531
|
+
if (boolArg(kwargs.think)) await setFeatureToggle(p, "think", true);
|
|
532
|
+
if (boolArg(kwargs.research)) await setFeatureToggle(p, "research", true);
|
|
533
|
+
await sendQwenMessage(p, prompt);
|
|
534
|
+
const answer = await waitForQwenAnswer(p, prompt, timeout);
|
|
535
|
+
return [
|
|
536
|
+
{ Role: "User", Text: prompt },
|
|
537
|
+
...mapQwenBubbles([answer], normalizeQwenBoolean(kwargs.markdown)),
|
|
538
|
+
];
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
cli({
|
|
543
|
+
site: "qwen",
|
|
544
|
+
name: "read",
|
|
545
|
+
description: "Read messages in the current Qwen conversation",
|
|
546
|
+
domain: "www.qianwen.com",
|
|
547
|
+
strategy: Strategy.COOKIE,
|
|
548
|
+
browser: true,
|
|
549
|
+
args: [{ name: "markdown", type: "bool", default: false }],
|
|
550
|
+
columns: ["Role", "Text"],
|
|
551
|
+
func: async (page, kwargs) => {
|
|
552
|
+
const p = page as QwenBrowserPage;
|
|
553
|
+
await ensureOnQwen(p);
|
|
554
|
+
await dismissLoginModal(p);
|
|
555
|
+
await p.wait(2);
|
|
556
|
+
const rows = mapQwenBubbles(
|
|
557
|
+
await getMessageBubbles(p),
|
|
558
|
+
normalizeQwenBoolean(kwargs.markdown),
|
|
559
|
+
);
|
|
560
|
+
if (!rows.length) throw new Error("No visible Qwen messages found.");
|
|
561
|
+
return rows;
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
cli({
|
|
566
|
+
site: "qwen",
|
|
567
|
+
name: "send",
|
|
568
|
+
description: "Send a prompt to Qwen without waiting for the reply",
|
|
569
|
+
domain: "www.qianwen.com",
|
|
570
|
+
strategy: Strategy.COOKIE,
|
|
571
|
+
browser: true,
|
|
572
|
+
args: [
|
|
573
|
+
{ name: "prompt", type: "str", required: true, positional: true },
|
|
574
|
+
{ name: "new", type: "bool", default: false },
|
|
575
|
+
{ name: "think", type: "bool", default: false },
|
|
576
|
+
{ name: "research", type: "bool", default: false },
|
|
577
|
+
],
|
|
578
|
+
columns: ["Status", "Prompt"],
|
|
579
|
+
func: async (page, kwargs) => {
|
|
580
|
+
const prompt = str(kwargs.prompt).trim();
|
|
581
|
+
if (!prompt) throw new Error("Qwen prompt cannot be empty.");
|
|
582
|
+
const p = page as QwenBrowserPage;
|
|
583
|
+
await ensureOnQwen(p);
|
|
584
|
+
await dismissLoginModal(p);
|
|
585
|
+
if (boolArg(kwargs.new)) await startNewChat(p);
|
|
586
|
+
if (boolArg(kwargs.think)) await setFeatureToggle(p, "think", true);
|
|
587
|
+
if (boolArg(kwargs.research)) await setFeatureToggle(p, "research", true);
|
|
588
|
+
await sendQwenMessage(p, prompt);
|
|
589
|
+
return [{ Status: "sent", Prompt: prompt }];
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
cli({
|
|
594
|
+
site: "qwen",
|
|
595
|
+
name: "history",
|
|
596
|
+
description: "List recent Qwen conversations",
|
|
597
|
+
domain: "www.qianwen.com",
|
|
598
|
+
strategy: Strategy.COOKIE,
|
|
599
|
+
browser: true,
|
|
600
|
+
args: [{ name: "limit", type: "int", default: 20 }],
|
|
601
|
+
columns: ["Index", "Title", "Updated", "Url"],
|
|
602
|
+
func: async (page, kwargs) => {
|
|
603
|
+
const limit = qwenBoundedInt(kwargs.limit, 20, 100, "history limit");
|
|
604
|
+
const p = page as QwenBrowserPage;
|
|
605
|
+
await ensureOnQwen(p);
|
|
606
|
+
await dismissLoginModal(p);
|
|
607
|
+
return getSessionList(p, limit);
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
cli({
|
|
612
|
+
site: "qwen",
|
|
613
|
+
name: "detail",
|
|
614
|
+
description: "Open a Qwen conversation by ID and read its messages",
|
|
615
|
+
domain: "www.qianwen.com",
|
|
616
|
+
strategy: Strategy.COOKIE,
|
|
617
|
+
browser: true,
|
|
618
|
+
args: [
|
|
619
|
+
{ name: "id", type: "str", required: true, positional: true },
|
|
620
|
+
{ name: "markdown", type: "bool", default: false },
|
|
621
|
+
],
|
|
622
|
+
columns: ["Role", "Text"],
|
|
623
|
+
func: async (page, kwargs) => {
|
|
624
|
+
const p = page as QwenBrowserPage;
|
|
625
|
+
const id = parseQwenSessionId(kwargs.id);
|
|
626
|
+
await p.goto(`${QWEN_HOME}chat/${id}`, {
|
|
627
|
+
waitUntil: "load",
|
|
628
|
+
settleMs: 2500,
|
|
629
|
+
});
|
|
630
|
+
await dismissLoginModal(p);
|
|
631
|
+
const startedAt = Date.now();
|
|
632
|
+
while (Date.now() - startedAt < 20_000) {
|
|
633
|
+
const rows = mapQwenBubbles(
|
|
634
|
+
await getMessageBubbles(p),
|
|
635
|
+
normalizeQwenBoolean(kwargs.markdown),
|
|
636
|
+
);
|
|
637
|
+
if (rows.length) return rows;
|
|
638
|
+
await p.wait(1);
|
|
639
|
+
}
|
|
640
|
+
throw new Error(`No visible Qwen messages found for conversation ${id}.`);
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
cli({
|
|
645
|
+
site: "qwen",
|
|
646
|
+
name: "new",
|
|
647
|
+
description: "Start a new conversation in Qwen",
|
|
648
|
+
domain: "www.qianwen.com",
|
|
649
|
+
strategy: Strategy.COOKIE,
|
|
650
|
+
browser: true,
|
|
651
|
+
columns: ["Status"],
|
|
652
|
+
func: async (page) => {
|
|
653
|
+
const p = page as QwenBrowserPage;
|
|
654
|
+
await p.goto(QWEN_HOME, { waitUntil: "load", settleMs: 2500 });
|
|
655
|
+
await dismissLoginModal(p);
|
|
656
|
+
await startNewChat(p);
|
|
657
|
+
return [{ Status: "New chat started" }];
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
cli({
|
|
662
|
+
site: "qwen",
|
|
663
|
+
name: "status",
|
|
664
|
+
description:
|
|
665
|
+
"Check Qwen page availability, login state, current session and model",
|
|
666
|
+
domain: "www.qianwen.com",
|
|
667
|
+
strategy: Strategy.COOKIE,
|
|
668
|
+
browser: true,
|
|
669
|
+
columns: ["Status", "Login", "Model", "SessionId", "Url"],
|
|
670
|
+
func: async (page) => {
|
|
671
|
+
const p = page as QwenBrowserPage;
|
|
672
|
+
await ensureOnQwen(p);
|
|
673
|
+
await p.wait(2);
|
|
674
|
+
const [loggedIn, sessionId, model, url] = await Promise.all([
|
|
675
|
+
isLoggedIn(p),
|
|
676
|
+
p.evaluate(`(() => {
|
|
677
|
+
const match = window.location.href.match(/\\/chat\\/([A-Za-z0-9_-]+)/);
|
|
678
|
+
return match ? match[1] : '';
|
|
679
|
+
})()`),
|
|
680
|
+
p.evaluate(`(() => {
|
|
681
|
+
${VISIBLE_JS}
|
|
682
|
+
const trigger = Array.from(document.querySelectorAll('[aria-haspopup=dialog]'))
|
|
683
|
+
.find((node) => isVisible(node) && (node.innerText || '').includes('Qwen'));
|
|
684
|
+
return trigger ? (trigger.innerText || '').split('\\n')[0].trim() : '';
|
|
685
|
+
})()`),
|
|
686
|
+
currentUrl(p),
|
|
687
|
+
]);
|
|
688
|
+
return [
|
|
689
|
+
{
|
|
690
|
+
Status: "Connected",
|
|
691
|
+
Login: loggedIn ? "Yes" : "No (guest mode)",
|
|
692
|
+
Model: str(model) || null,
|
|
693
|
+
SessionId: str(sessionId) || null,
|
|
694
|
+
Url: url,
|
|
695
|
+
},
|
|
696
|
+
];
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
cli({
|
|
701
|
+
site: "qwen",
|
|
702
|
+
name: "image",
|
|
703
|
+
description: "Generate images with Qwen and save them locally",
|
|
704
|
+
domain: "www.qianwen.com",
|
|
705
|
+
strategy: Strategy.COOKIE,
|
|
706
|
+
browser: true,
|
|
707
|
+
args: [
|
|
708
|
+
{ name: "prompt", type: "str", required: true, positional: true },
|
|
709
|
+
{ name: "op", type: "str", default: "~/Pictures/qianwen" },
|
|
710
|
+
{ name: "new", type: "bool", default: true },
|
|
711
|
+
{ name: "sd", type: "bool", default: false },
|
|
712
|
+
{ name: "timeout", type: "int", default: 180 },
|
|
713
|
+
],
|
|
714
|
+
columns: ["Status", "File", "Link"],
|
|
715
|
+
func: async (page, kwargs) => {
|
|
716
|
+
const prompt = str(kwargs.prompt).trim();
|
|
717
|
+
if (!prompt) throw new Error("Qwen image prompt cannot be empty.");
|
|
718
|
+
const p = page as QwenBrowserPage;
|
|
719
|
+
const timeout = qwenBoundedInt(kwargs.timeout, 180, 600, "image timeout");
|
|
720
|
+
await ensureOnQwen(p);
|
|
721
|
+
await dismissLoginModal(p);
|
|
722
|
+
if (normalizeQwenBoolean(kwargs.new, true)) await startNewChat(p);
|
|
723
|
+
await setFeatureToggle(p, "image", true);
|
|
724
|
+
await sendQwenMessage(p, prompt);
|
|
725
|
+
let assistantId = "";
|
|
726
|
+
for (let i = 0; i < 5; i += 1) {
|
|
727
|
+
await p.wait(1);
|
|
728
|
+
const assistant = [...(await getMessageBubbles(p))]
|
|
729
|
+
.reverse()
|
|
730
|
+
.find((bubble) => bubble.role === "Assistant");
|
|
731
|
+
if (assistant) {
|
|
732
|
+
assistantId = str(assistant.id);
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const link = await currentUrl(p);
|
|
737
|
+
if (boolArg(kwargs.sd))
|
|
738
|
+
return [{ Status: "generated", File: null, Link: link }];
|
|
739
|
+
const urls = await waitForImageUrls(p, assistantId, timeout);
|
|
740
|
+
const outputDir = qwenOutputDir(kwargs.op);
|
|
741
|
+
mkdirSync(outputDir, { recursive: true });
|
|
742
|
+
const stamp = Date.now();
|
|
743
|
+
const rows: Record<string, unknown>[] = [];
|
|
744
|
+
for (const [index, url] of urls.entries()) {
|
|
745
|
+
const asset = await fetchImageAsset(p, url);
|
|
746
|
+
const suffix = urls.length > 1 ? `_${index + 1}` : "";
|
|
747
|
+
const filePath = join(
|
|
748
|
+
outputDir,
|
|
749
|
+
`qwen_${stamp}${suffix}${qwenImageExt(asset.mime)}`,
|
|
750
|
+
);
|
|
751
|
+
writeFileSync(filePath, Buffer.from(asset.base64, "base64"));
|
|
752
|
+
rows.push({ Status: "saved", File: displayPath(filePath), Link: link });
|
|
753
|
+
}
|
|
754
|
+
if (!rows.length)
|
|
755
|
+
throw new Error("No generated Qwen images were available to download.");
|
|
756
|
+
return rows;
|
|
757
|
+
},
|
|
758
|
+
});
|