@jackwener/opencli 1.7.12 → 1.7.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/README.zh-CN.md +9 -8
- package/cli-manifest.json +12128 -6665
- package/clis/1point3acres/digest.js +35 -0
- package/clis/1point3acres/forum.js +51 -0
- package/clis/1point3acres/forums.js +44 -0
- package/clis/1point3acres/hot.js +35 -0
- package/clis/1point3acres/latest.js +35 -0
- package/clis/1point3acres/notifications.js +64 -0
- package/clis/1point3acres/search.js +71 -0
- package/clis/1point3acres/thread.js +117 -0
- package/clis/1point3acres/user.js +77 -0
- package/clis/1point3acres/utils.js +247 -0
- package/clis/_shared/desktop-commands.js +4 -0
- package/clis/aibase/news.js +110 -0
- package/clis/aibase/news.test.js +59 -0
- package/clis/amazon/discussion.test.js +1 -28
- package/clis/antigravity/watch.js +3 -2
- package/clis/arxiv/author.js +44 -0
- package/clis/baidu-scholar/search.js +0 -1
- package/clis/bbc/topic.js +57 -0
- package/clis/bbc/utils.js +79 -0
- package/clis/chaoxing/assignments.js +1 -1
- package/clis/chaoxing/exams.js +1 -1
- package/clis/chatgpt/ask.js +57 -0
- package/clis/chatgpt/commands.test.js +45 -0
- package/clis/chatgpt/detail.js +46 -0
- package/clis/chatgpt/history.js +39 -0
- package/clis/chatgpt/image.js +12 -11
- package/clis/chatgpt/image.test.js +23 -0
- package/clis/chatgpt/new.js +25 -0
- package/clis/chatgpt/read.js +43 -0
- package/clis/chatgpt/send.js +46 -0
- package/clis/chatgpt/status.js +29 -0
- package/clis/chatgpt/utils.js +294 -4
- package/clis/chatgpt/utils.test.js +13 -0
- package/clis/chatgpt-app/ask.js +6 -3
- package/clis/chatwise/ask.js +16 -43
- package/clis/chatwise/composer.test.js +186 -0
- package/clis/chatwise/send.js +2 -24
- package/clis/chatwise/utils.js +143 -0
- package/clis/claude/ask.js +1 -1
- package/clis/claude/detail.js +1 -0
- package/clis/claude/history.js +1 -0
- package/clis/claude/new.js +1 -0
- package/clis/claude/read.js +1 -0
- package/clis/claude/send.js +1 -0
- package/clis/claude/status.js +1 -0
- package/clis/codex/ask.js +15 -9
- package/clis/codex/history.js +16 -33
- package/clis/codex/projects.js +28 -0
- package/clis/codex/read.js +10 -4
- package/clis/codex/send.js +10 -3
- package/clis/codex/sidebar.js +356 -0
- package/clis/codex/sidebar.test.js +329 -0
- package/clis/coingecko/categories.js +75 -0
- package/clis/coingecko/coin.js +107 -0
- package/clis/coingecko/coingecko.test.js +109 -0
- package/clis/coingecko/derivatives.js +84 -0
- package/clis/coingecko/exchanges.js +74 -0
- package/clis/coingecko/global.js +71 -0
- package/clis/coingecko/top.js +64 -0
- package/clis/coingecko/trending.js +55 -0
- package/clis/coupang/add-to-cart.js +21 -13
- package/clis/coupang/coupang.test.js +159 -0
- package/clis/coupang/product.js +257 -0
- package/clis/coupang/search.js +38 -16
- package/clis/coupang/utils.js +55 -1
- package/clis/crates/crate.js +62 -0
- package/clis/crates/search.js +44 -0
- package/clis/crates/utils.js +72 -0
- package/clis/ctrip/ctrip.test.js +234 -0
- package/clis/ctrip/hotel-suggest.js +45 -0
- package/clis/ctrip/search.js +22 -68
- package/clis/ctrip/utils.js +175 -0
- package/clis/cursor/ask.js +6 -3
- package/clis/dblp/author.js +133 -0
- package/clis/dblp/venue.js +64 -0
- package/clis/deepseek/ask.js +12 -7
- package/clis/deepseek/ask.test.js +13 -13
- package/clis/deepseek/detail.js +38 -0
- package/clis/deepseek/detail.test.js +81 -0
- package/clis/deepseek/history.js +1 -0
- package/clis/deepseek/new.js +1 -0
- package/clis/deepseek/read.js +1 -0
- package/clis/deepseek/send.js +140 -0
- package/clis/deepseek/send.test.js +107 -0
- package/clis/deepseek/status.js +1 -0
- package/clis/deepseek/utils.js +66 -0
- package/clis/deepseek/utils.test.js +107 -1
- package/clis/defillama/defillama.test.js +99 -0
- package/clis/defillama/protocol.js +84 -0
- package/clis/defillama/protocols.js +55 -0
- package/clis/defillama/utils.js +99 -0
- package/clis/devto/latest.js +74 -0
- package/clis/dockerhub/image.js +52 -0
- package/clis/dockerhub/search.js +47 -0
- package/clis/dockerhub/utils.js +100 -0
- package/clis/doubao/ask.js +7 -3
- package/clis/doubao/detail.js +1 -0
- package/clis/doubao/history.js +1 -0
- package/clis/doubao/meeting-summary.js +1 -0
- package/clis/doubao/meeting-transcript.js +1 -0
- package/clis/doubao/new.js +1 -0
- package/clis/doubao/read.js +1 -0
- package/clis/doubao/send.js +1 -0
- package/clis/doubao/status.js +1 -0
- package/clis/douyin/draft.test.js +1 -30
- package/clis/endoflife/endoflife.test.js +51 -0
- package/clis/endoflife/product.js +55 -0
- package/clis/endoflife/utils.js +89 -0
- package/clis/facebook/__fixtures__/notifications-page.html +13 -0
- package/clis/facebook/notifications.js +326 -30
- package/clis/facebook/notifications.test.js +458 -0
- package/clis/flathub/app.js +71 -0
- package/clis/flathub/flathub.test.js +90 -0
- package/clis/flathub/search.js +80 -0
- package/clis/flathub/utils.js +114 -0
- package/clis/gemini/ask.js +7 -3
- package/clis/gemini/ask.test.js +2 -2
- package/clis/gemini/deep-research-result.js +6 -2
- package/clis/gemini/deep-research-result.test.js +15 -14
- package/clis/gemini/deep-research.js +8 -4
- package/clis/gemini/deep-research.test.js +15 -18
- package/clis/gemini/image.js +7 -2
- package/clis/gemini/new.js +1 -0
- package/clis/gemini/utils.js +0 -4
- package/clis/google-scholar/cite.js +0 -1
- package/clis/google-scholar/profile.js +0 -1
- package/clis/google-scholar/search.js +0 -1
- package/clis/goproxy/goproxy.test.js +103 -0
- package/clis/goproxy/module.js +47 -0
- package/clis/goproxy/utils.js +165 -0
- package/clis/goproxy/versions.js +59 -0
- package/clis/gov-law/recent.js +0 -1
- package/clis/gov-law/search.js +0 -1
- package/clis/gov-policy/__fixtures__/recent.html +16 -0
- package/clis/gov-policy/__fixtures__/search.html +41 -0
- package/clis/gov-policy/gov-policy.test.js +224 -0
- package/clis/gov-policy/recent.js +66 -24
- package/clis/gov-policy/search.js +65 -23
- package/clis/gov-policy/utils.js +54 -0
- package/clis/grok/ask.js +49 -265
- package/clis/grok/ask.test.js +21 -46
- package/clis/grok/detail.js +60 -0
- package/clis/grok/history.js +48 -0
- package/clis/grok/{image.ts → image.js} +56 -70
- package/clis/grok/image.test.ts +20 -0
- package/clis/grok/new.js +20 -0
- package/clis/grok/read.js +39 -0
- package/clis/grok/send.js +50 -0
- package/clis/grok/status.js +41 -0
- package/clis/grok/utils.js +326 -0
- package/clis/grok/utils.test.js +103 -0
- package/clis/hf/datasets.js +88 -0
- package/clis/hf/hf.test.js +16 -0
- package/clis/hf/models.js +91 -0
- package/clis/hf/paper.js +79 -0
- package/clis/hf/spaces.js +101 -0
- package/clis/hf/top.js +1 -0
- package/clis/homebrew/cask.js +39 -0
- package/clis/homebrew/formula.js +41 -0
- package/clis/homebrew/popular.js +54 -0
- package/clis/homebrew/utils.js +100 -0
- package/clis/hupu/__fixtures__/hot-home.html +64 -0
- package/clis/hupu/detail.js +0 -1
- package/clis/hupu/hot.js +156 -35
- package/clis/hupu/hot.test.js +224 -0
- package/clis/hupu/search.js +0 -1
- package/clis/instagram/note.js +1 -1
- package/clis/instagram/note.test.js +1 -29
- package/clis/instagram/post.js +1 -1
- package/clis/instagram/post.test.js +1 -1
- package/clis/instagram/reel.js +1 -1
- package/clis/instagram/story.js +1 -1
- package/clis/instagram/story.test.js +1 -34
- package/clis/jd/commands.test.js +1 -24
- package/clis/lichess/lichess.test.js +85 -0
- package/clis/lichess/top.js +46 -0
- package/clis/lichess/user.js +91 -0
- package/clis/lichess/utils.js +97 -0
- package/clis/linkedin/search.js +107 -10
- package/clis/linkedin/search.test.js +222 -0
- package/clis/linux-do/feed.js +2 -5
- package/clis/linux-do/feed.test.js +35 -0
- package/clis/lobsters/domain.js +92 -0
- package/clis/maven/artifact.js +49 -0
- package/clis/maven/search.js +51 -0
- package/clis/maven/utils.js +110 -0
- package/clis/mdn/search.js +97 -0
- package/clis/medium/tag.js +135 -0
- package/clis/npm/downloads.js +59 -0
- package/clis/npm/package.js +70 -0
- package/clis/npm/search.js +49 -0
- package/clis/npm/utils.js +76 -0
- package/clis/nuget/nuget.test.js +111 -0
- package/clis/nuget/package.js +101 -0
- package/clis/nuget/search.js +69 -0
- package/clis/nuget/utils.js +87 -0
- package/clis/nvd/cve.js +121 -0
- package/clis/oeis/oeis.test.js +88 -0
- package/clis/oeis/search.js +63 -0
- package/clis/oeis/sequence.js +71 -0
- package/clis/oeis/utils.js +88 -0
- package/clis/openalex/search.js +69 -0
- package/clis/openalex/utils.js +160 -0
- package/clis/openalex/work.js +65 -0
- package/clis/openfda/drug-label.js +74 -0
- package/clis/openfda/food-recall.js +65 -0
- package/clis/openfda/openfda.test.js +114 -0
- package/clis/openfda/utils.js +67 -0
- package/clis/osv/osv.test.js +97 -0
- package/clis/osv/query.js +72 -0
- package/clis/osv/utils.js +169 -0
- package/clis/osv/vulnerability.js +54 -0
- package/clis/packagist/package.js +49 -0
- package/clis/packagist/search.js +43 -0
- package/clis/packagist/utils.js +113 -0
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/pixiv/download.test.js +1 -1
- package/clis/pixiv/illusts.test.js +1 -1
- package/clis/pixiv/search.test.js +1 -1
- package/clis/pubmed/article.js +50 -0
- package/clis/pubmed/author.js +64 -0
- package/clis/pubmed/citations.js +36 -0
- package/clis/pubmed/pubmed.test.js +276 -0
- package/clis/pubmed/related.js +45 -0
- package/clis/pubmed/search.js +75 -0
- package/clis/pubmed/utils.js +309 -0
- package/clis/pypi/downloads.js +66 -0
- package/clis/pypi/package.js +79 -0
- package/clis/pypi/utils.js +55 -0
- package/clis/quark/mv.js +1 -1
- package/clis/quark/save.js +1 -1
- package/clis/qwen/ask.js +85 -0
- package/clis/qwen/detail.js +62 -0
- package/clis/qwen/history.js +61 -0
- package/clis/qwen/image.js +179 -0
- package/clis/qwen/new.js +23 -0
- package/clis/qwen/read.js +41 -0
- package/clis/qwen/send.js +55 -0
- package/clis/qwen/status.js +37 -0
- package/clis/qwen/utils.js +409 -0
- package/clis/qwen/utils.test.js +45 -0
- package/clis/rest-countries/country.js +65 -0
- package/clis/rest-countries/region.js +64 -0
- package/clis/rest-countries/rest-countries.test.js +83 -0
- package/clis/rest-countries/utils.js +126 -0
- package/clis/reuters/article-detail.js +53 -0
- package/clis/reuters/reuters.test.js +299 -0
- package/clis/reuters/search.js +45 -34
- package/clis/reuters/utils.js +159 -0
- package/clis/rfc/rfc.js +52 -0
- package/clis/rfc/rfc.test.js +74 -0
- package/clis/rfc/utils.js +72 -0
- package/clis/rubygems/gem.js +42 -0
- package/clis/rubygems/search.js +47 -0
- package/clis/rubygems/utils.js +86 -0
- package/clis/stackoverflow/related.js +66 -0
- package/clis/stackoverflow/stackoverflow.test.js +58 -0
- package/clis/stackoverflow/tag.js +60 -0
- package/clis/stackoverflow/user.js +50 -0
- package/clis/stackoverflow/utils.js +118 -0
- package/clis/steam/app.js +67 -0
- package/clis/steam/search.js +58 -0
- package/clis/steam/steam.test.js +46 -0
- package/clis/steam/utils.js +107 -0
- package/clis/taobao/commands.test.js +1 -24
- package/clis/test-utils.js +61 -0
- package/clis/tieba/hot.js +0 -1
- package/clis/tiktok/comment.js +128 -41
- package/clis/tiktok/creator-videos.js +270 -0
- package/clis/tiktok/creator-videos.test.js +113 -0
- package/clis/tiktok/explore.js +137 -29
- package/clis/tiktok/follow.js +115 -33
- package/clis/tiktok/following.js +157 -36
- package/clis/tiktok/friends.js +139 -37
- package/clis/tiktok/live.js +137 -41
- package/clis/tiktok/notifications.js +141 -38
- package/clis/tiktok/refactor.test.js +389 -0
- package/clis/tiktok/unfollow.js +124 -38
- package/clis/tiktok/user.js +203 -29
- package/clis/tiktok/utils.js +505 -0
- package/clis/tiktok/write-refactor.test.js +370 -0
- package/clis/toutiao/articles.js +36 -62
- package/clis/toutiao/hot.js +63 -0
- package/clis/toutiao/toutiao.test.js +378 -0
- package/clis/toutiao/utils.js +161 -0
- package/clis/tvmaze/search.js +61 -0
- package/clis/tvmaze/show.js +60 -0
- package/clis/tvmaze/tvmaze.test.js +93 -0
- package/clis/tvmaze/utils.js +110 -0
- package/clis/twitter/accept.js +1 -1
- package/clis/twitter/followers.js +134 -69
- package/clis/twitter/quote.js +139 -0
- package/clis/twitter/quote.test.js +106 -0
- package/clis/twitter/reply-dm.js +1 -1
- package/clis/twitter/reply.test.js +1 -29
- package/clis/twitter/retweet.js +99 -0
- package/clis/twitter/retweet.test.js +69 -0
- package/clis/twitter/shared.js +38 -0
- package/clis/twitter/shared.test.js +28 -1
- package/clis/twitter/unlike.js +87 -0
- package/clis/twitter/unlike.test.js +72 -0
- package/clis/twitter/unretweet.js +99 -0
- package/clis/twitter/unretweet.test.js +69 -0
- package/clis/uisdc/news.js +105 -0
- package/clis/uisdc/news.test.js +66 -0
- package/clis/wanfang/search.js +0 -1
- package/clis/web/read.js +47 -17
- package/clis/web/read.test.js +101 -1
- package/clis/weixin/create-draft.js +1 -1
- package/clis/weixin/drafts.js +1 -1
- package/clis/weixin/drafts.test.js +5 -1
- package/clis/weixin/search.js +157 -0
- package/clis/weixin/search.test.js +227 -0
- package/clis/wikidata/entity.js +60 -0
- package/clis/wikidata/search.js +50 -0
- package/clis/wikidata/utils.js +117 -0
- package/clis/wikidata/wikidata.test.js +83 -0
- package/clis/wikipedia/page.js +95 -0
- package/clis/wttr/current.js +63 -0
- package/clis/wttr/forecast.js +71 -0
- package/clis/wttr/utils.js +50 -0
- package/clis/wttr/wttr.test.js +84 -0
- package/clis/xianyu/chat.js +16 -4
- package/clis/xianyu/chat.test.js +64 -0
- package/clis/xianyu/publish.js +485 -0
- package/clis/xianyu/publish.test.js +220 -0
- package/clis/xiaoe/catalog.js +105 -40
- package/clis/xiaoe/content.js +164 -29
- package/clis/xiaoe/courses.js +86 -29
- package/clis/xiaoe/xiaoe.test.js +486 -0
- package/clis/xiaohongshu/creator-notes-summary.js +1 -1
- package/clis/xiaohongshu/publish.js +16 -3
- package/clis/xiaohongshu/publish.test.js +46 -1
- package/clis/youtube/transcript.js +13 -19
- package/clis/youtube/transcript.test.js +17 -0
- package/clis/yuanbao/ask.js +17 -66
- package/clis/yuanbao/ask.test.js +5 -5
- package/clis/yuanbao/detail.js +65 -0
- package/clis/yuanbao/history.js +51 -0
- package/clis/yuanbao/new.js +1 -0
- package/clis/yuanbao/read.js +38 -0
- package/clis/yuanbao/send.js +57 -0
- package/clis/yuanbao/shared.js +297 -5
- package/clis/yuanbao/shared.test.js +80 -0
- package/clis/yuanbao/status.js +44 -0
- package/clis/zlibrary/commands.test.js +1 -11
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +44 -1
- package/dist/src/browser/base-page.test.js +66 -0
- package/dist/src/browser/bridge.js +47 -45
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +51 -9
- package/dist/src/browser/daemon-client.d.ts +4 -0
- package/dist/src/browser/errors.js +1 -1
- package/dist/src/browser/page.d.ts +1 -1
- package/dist/src/browser/page.js +3 -1
- package/dist/src/browser/page.test.js +29 -0
- package/dist/src/browser/target-errors.d.ts +2 -1
- package/dist/src/browser/target-errors.js +1 -0
- package/dist/src/browser/target-resolver.d.ts +25 -0
- package/dist/src/browser/target-resolver.js +43 -0
- package/dist/src/browser.test.js +18 -0
- package/dist/src/build-manifest.js +9 -4
- package/dist/src/build-manifest.test.js +2 -8
- package/dist/src/capabilityRouting.d.ts +16 -1
- package/dist/src/capabilityRouting.js +24 -1
- package/dist/src/capabilityRouting.test.js +19 -1
- package/dist/src/cli.js +76 -11
- package/dist/src/cli.test.js +241 -1
- package/dist/src/commanderAdapter.js +23 -9
- package/dist/src/commanderAdapter.test.js +0 -1
- package/dist/src/discovery.js +2 -5
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +111 -27
- package/dist/src/execution.test.js +326 -17
- package/dist/src/help.d.ts +27 -2
- package/dist/src/help.js +196 -23
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +54 -0
- package/dist/src/main.js +14 -1
- package/dist/src/manifest-types.d.ts +5 -3
- package/dist/src/pipeline/executor.js +1 -1
- package/dist/src/pipeline/executor.test.js +8 -0
- package/dist/src/pipeline/registry.d.ts +9 -0
- package/dist/src/pipeline/registry.js +13 -1
- package/dist/src/pipeline/steps/browser.d.ts +1 -0
- package/dist/src/pipeline/steps/browser.js +10 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +12 -11
- package/dist/src/registry.js +16 -6
- package/dist/src/registry.test.js +2 -2
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/serialization.d.ts +2 -2
- package/dist/src/serialization.js +4 -6
- package/dist/src/serialization.test.js +17 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/validate.js +15 -11
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/package.json +1 -1
- package/scripts/fetch-adapters.js +1 -1
- package/scripts/typed-error-lint-baseline.json +5 -77
- package/clis/ctrip/search.test.js +0 -64
- package/clis/gov-policy/commands.test.js +0 -27
- package/clis/linux-do/category.js +0 -37
- package/clis/linux-do/hot.js +0 -26
- package/clis/linux-do/latest.js +0 -19
- package/clis/pixiv/test-utils.js +0 -23
- package/clis/toutiao/articles.test.js +0 -30
- package/dist/src/analysis.d.ts +0 -40
- package/dist/src/analysis.js +0 -172
package/clis/yuanbao/shared.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { AuthRequiredError } from '@jackwener/opencli/errors';
|
|
1
|
+
import { ArgumentError, AuthRequiredError } from '@jackwener/opencli/errors';
|
|
2
|
+
|
|
2
3
|
export const YUANBAO_DOMAIN = 'yuanbao.tencent.com';
|
|
3
4
|
export const YUANBAO_URL = 'https://yuanbao.tencent.com/';
|
|
4
5
|
const SESSION_HINT = 'Likely login/auth/challenge/session issue in the existing yuanbao.tencent.com browser session.';
|
|
6
|
+
|
|
7
|
+
const AGENT_ID_RE = /^[A-Za-z0-9_-]{4,40}$/;
|
|
8
|
+
const CONV_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* Reusable visibility check for injected browser scripts.
|
|
7
12
|
* Embed in page.evaluate strings via `${IS_VISIBLE_JS}`.
|
|
@@ -15,27 +20,36 @@ export const IS_VISIBLE_JS = `const isVisible = (node) => {
|
|
|
15
20
|
&& style.display !== 'none'
|
|
16
21
|
&& style.visibility !== 'hidden';
|
|
17
22
|
};`;
|
|
23
|
+
|
|
18
24
|
export function authRequired(message) {
|
|
19
25
|
return new AuthRequiredError(YUANBAO_DOMAIN, `${message} ${SESSION_HINT}`);
|
|
20
26
|
}
|
|
27
|
+
|
|
28
|
+
export function normalizeBooleanFlag(value, fallback = false) {
|
|
29
|
+
if (typeof value === 'boolean') return value;
|
|
30
|
+
if (value == null || value === '') return fallback;
|
|
31
|
+
const normalized = String(value).trim().toLowerCase();
|
|
32
|
+
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
export async function isOnYuanbao(page) {
|
|
22
36
|
const url = await page.evaluate('window.location.href').catch(() => '');
|
|
23
|
-
if (typeof url !== 'string' || !url)
|
|
24
|
-
return false;
|
|
37
|
+
if (typeof url !== 'string' || !url) return false;
|
|
25
38
|
try {
|
|
26
39
|
const hostname = new URL(url).hostname;
|
|
27
40
|
return hostname === YUANBAO_DOMAIN || hostname.endsWith(`.${YUANBAO_DOMAIN}`);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
41
|
+
} catch {
|
|
30
42
|
return false;
|
|
31
43
|
}
|
|
32
44
|
}
|
|
45
|
+
|
|
33
46
|
export async function ensureYuanbaoPage(page) {
|
|
34
47
|
if (!(await isOnYuanbao(page))) {
|
|
35
48
|
await page.goto(YUANBAO_URL, { waitUntil: 'load', settleMs: 2500 });
|
|
36
49
|
await page.wait(1);
|
|
37
50
|
}
|
|
38
51
|
}
|
|
52
|
+
|
|
39
53
|
export async function hasLoginGate(page) {
|
|
40
54
|
const result = await page.evaluate(`(() => {
|
|
41
55
|
const bodyText = document.body.innerText || '';
|
|
@@ -47,3 +61,281 @@ export async function hasLoginGate(page) {
|
|
|
47
61
|
})()`);
|
|
48
62
|
return Boolean(result);
|
|
49
63
|
}
|
|
64
|
+
|
|
65
|
+
export async function isLoggedIn(page) {
|
|
66
|
+
return !(await hasLoginGate(page));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract Yuanbao session identity from a raw input.
|
|
71
|
+
*
|
|
72
|
+
* Yuanbao chat URLs are `/chat/<agentId>/<convId>`. Both parts are required
|
|
73
|
+
* to navigate — there is no stable default agentId we can fall back to. So we
|
|
74
|
+
* only accept inputs that resolve a complete `{agentId, convId}` pair:
|
|
75
|
+
* - full `https://yuanbao.tencent.com/chat/<agentId>/<convId>` URL
|
|
76
|
+
* - bare slash form `<agentId>/<convId>`
|
|
77
|
+
*
|
|
78
|
+
* A bare convId UUID is rejected with an actionable message — opening the
|
|
79
|
+
* wrong agent silently is a much worse failure mode than throwing.
|
|
80
|
+
*
|
|
81
|
+
* The trailing `(?:[/?#]|$)` boundary in the URL regex prevents over-long
|
|
82
|
+
* suffixes (e.g. `<id>extra`) from silently truncating to a valid-looking ID.
|
|
83
|
+
*/
|
|
84
|
+
export function parseYuanbaoSessionId(input) {
|
|
85
|
+
const raw = String(input ?? '').trim();
|
|
86
|
+
if (!raw) {
|
|
87
|
+
throw new ArgumentError(
|
|
88
|
+
'id',
|
|
89
|
+
'must be a non-empty Yuanbao chat URL or "<agentId>/<convId>" pair',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const urlMatch = raw.match(/yuanbao\.tencent\.com\/chat\/([A-Za-z0-9_-]+)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[/?#]|$)/i);
|
|
93
|
+
if (urlMatch) {
|
|
94
|
+
const [, agentId, convId] = urlMatch;
|
|
95
|
+
if (!AGENT_ID_RE.test(agentId) || !CONV_ID_RE.test(convId)) {
|
|
96
|
+
throw new ArgumentError(
|
|
97
|
+
'id',
|
|
98
|
+
`not a valid Yuanbao chat URL (got "${input}"); expected https://yuanbao.tencent.com/chat/<agentId>/<convId>`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return { agentId, convId: convId.toLowerCase() };
|
|
102
|
+
}
|
|
103
|
+
const slashMatch = raw.match(/^([A-Za-z0-9_-]+)\/([0-9a-f-]{36})$/i);
|
|
104
|
+
if (slashMatch) {
|
|
105
|
+
const [, agentId, convId] = slashMatch;
|
|
106
|
+
if (!AGENT_ID_RE.test(agentId) || !CONV_ID_RE.test(convId)) {
|
|
107
|
+
throw new ArgumentError(
|
|
108
|
+
'id',
|
|
109
|
+
`not a valid Yuanbao "<agentId>/<convId>" pair (got "${input}"); agentId must be 4-40 word chars, convId must be a UUID`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return { agentId, convId: convId.toLowerCase() };
|
|
113
|
+
}
|
|
114
|
+
throw new ArgumentError(
|
|
115
|
+
'id',
|
|
116
|
+
`not a valid Yuanbao session reference (got "${input}"); pass either a full https://yuanbao.tencent.com/chat/<agentId>/<convId> URL or a bare "<agentId>/<convId>" pair. A UUID alone is not enough — Yuanbao requires the agentId.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function getCurrentYuanbaoSessionId(page) {
|
|
121
|
+
const url = await page.evaluate('window.location.href').catch(() => '');
|
|
122
|
+
if (typeof url !== 'string') return null;
|
|
123
|
+
const match = url.match(/yuanbao\.tencent\.com\/chat\/([A-Za-z0-9_-]+)\/([0-9a-f-]{36})(?:[/?#]|$)/i);
|
|
124
|
+
if (!match) return null;
|
|
125
|
+
const [, agentId, convId] = match;
|
|
126
|
+
if (!AGENT_ID_RE.test(agentId) || !CONV_ID_RE.test(convId)) return null;
|
|
127
|
+
return { agentId, convId: convId.toLowerCase() };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function getYuanbaoModelLabel(page) {
|
|
131
|
+
const result = await page.evaluate(`(() => {
|
|
132
|
+
const btn = document.querySelector('[dt-button-id="model_switch"]');
|
|
133
|
+
if (!(btn instanceof HTMLElement)) return null;
|
|
134
|
+
const label = (btn.querySelector('.t-button__text')?.textContent || btn.textContent || '').trim();
|
|
135
|
+
const modelId = btn.getAttribute('dt-model-id') || '';
|
|
136
|
+
return { label, modelId };
|
|
137
|
+
})()`);
|
|
138
|
+
if (!result || typeof result !== 'object') return { label: null, modelId: null };
|
|
139
|
+
return {
|
|
140
|
+
label: typeof result.label === 'string' && result.label ? result.label : null,
|
|
141
|
+
modelId: typeof result.modelId === 'string' && result.modelId ? result.modelId : null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Read the current conversation transcript as `{id, role, text, html}` rows.
|
|
147
|
+
*
|
|
148
|
+
* Each `.agent-chat__list__item` carries `data-conv-id` (`<convId>_<idx>`),
|
|
149
|
+
* `data-conv-speaker` (`human`/`ai`), and `data-conv-idx`. We use those as the
|
|
150
|
+
* stable per-turn identity so polling/dedup logic in `ask` stays correct after
|
|
151
|
+
* future re-renders.
|
|
152
|
+
*
|
|
153
|
+
* Image-only assistant turns may have empty visible text; we keep them when
|
|
154
|
+
* `html` is non-empty so callers can render images downstream.
|
|
155
|
+
*/
|
|
156
|
+
export async function getYuanbaoMessageBubbles(page) {
|
|
157
|
+
const result = await page.evaluate(`(() => {
|
|
158
|
+
${IS_VISIBLE_JS}
|
|
159
|
+
const items = Array.from(document.querySelectorAll('.agent-chat__list__item--human, .agent-chat__list__item--ai'))
|
|
160
|
+
.filter((node) => isVisible(node));
|
|
161
|
+
return items.map((node, positional) => {
|
|
162
|
+
const isAi = node.classList.contains('agent-chat__list__item--ai');
|
|
163
|
+
const role = isAi ? 'Assistant' : 'User';
|
|
164
|
+
const id = node.getAttribute('data-conv-id') || ('pos-' + positional + (isAi ? '-ai' : '-human'));
|
|
165
|
+
const idx = Number(node.getAttribute('data-conv-idx') || positional + 1);
|
|
166
|
+
const contentNode = isAi
|
|
167
|
+
? (node.querySelector('.hyc-content-md-done')
|
|
168
|
+
|| node.querySelector('.hyc-content-md')
|
|
169
|
+
|| node.querySelector('.agent-chat__speech-text')
|
|
170
|
+
|| node.querySelector('.agent-chat__bubble__content'))
|
|
171
|
+
: (node.querySelector('.hyc-component-text .hyc-content-text')
|
|
172
|
+
|| node.querySelector('.hyc-content-text')
|
|
173
|
+
|| node.querySelector('.agent-chat__bubble__content'));
|
|
174
|
+
const html = contentNode instanceof HTMLElement ? (contentNode.innerHTML || '') : '';
|
|
175
|
+
const rawText = contentNode instanceof HTMLElement
|
|
176
|
+
? (contentNode.innerText || contentNode.textContent || '')
|
|
177
|
+
: ((node.innerText || node.textContent) || '');
|
|
178
|
+
return {
|
|
179
|
+
id,
|
|
180
|
+
idx,
|
|
181
|
+
role,
|
|
182
|
+
text: String(rawText || '').replace(/\\u00a0/g, ' ').trim(),
|
|
183
|
+
html,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
})()`);
|
|
187
|
+
if (!Array.isArray(result)) return [];
|
|
188
|
+
return result
|
|
189
|
+
.map((item) => ({
|
|
190
|
+
id: String(item?.id || ''),
|
|
191
|
+
idx: Number.isInteger(item?.idx) ? item.idx : 0,
|
|
192
|
+
role: item?.role === 'Assistant' ? 'Assistant' : 'User',
|
|
193
|
+
text: String(item?.text || '').trim(),
|
|
194
|
+
html: String(item?.html || ''),
|
|
195
|
+
}))
|
|
196
|
+
.filter((item) => item.id && (item.text || item.html));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Enumerate sidebar conversation entries.
|
|
201
|
+
*
|
|
202
|
+
* Each `.yb-recent-conv-list__item` exposes:
|
|
203
|
+
* - `dt-cid` — conversation UUID
|
|
204
|
+
* - `dt-agent-id`— agent slug
|
|
205
|
+
* - `[data-item-name]` — display title
|
|
206
|
+
*
|
|
207
|
+
* We do NOT trigger sidebar virtual scroll here — Yuanbao loads the visible
|
|
208
|
+
* window only, so callers requesting a higher `limit` than the rendered count
|
|
209
|
+
* get whatever is currently rendered. That matches Yuanbao's own UX.
|
|
210
|
+
*/
|
|
211
|
+
export async function getYuanbaoSessionList(page, limit) {
|
|
212
|
+
const cap = Number(limit ?? 20);
|
|
213
|
+
if (!Number.isInteger(cap) || cap <= 0) {
|
|
214
|
+
throw new ArgumentError('limit', 'must be a positive integer');
|
|
215
|
+
}
|
|
216
|
+
const result = await page.evaluate(`(() => {
|
|
217
|
+
${IS_VISIBLE_JS}
|
|
218
|
+
const nodes = Array.from(document.querySelectorAll('.yb-recent-conv-list__item'))
|
|
219
|
+
.filter((node) => isVisible(node));
|
|
220
|
+
return nodes.map((node) => {
|
|
221
|
+
const cid = node.getAttribute('dt-cid') || '';
|
|
222
|
+
const agentId = node.getAttribute('dt-agent-id') || '';
|
|
223
|
+
const titleEl = node.querySelector('[data-item-name]');
|
|
224
|
+
const title = (titleEl?.getAttribute('data-item-name') || titleEl?.textContent || '').trim();
|
|
225
|
+
return { cid, agentId, title };
|
|
226
|
+
});
|
|
227
|
+
})()`);
|
|
228
|
+
if (!Array.isArray(result)) return [];
|
|
229
|
+
return result
|
|
230
|
+
.map((item) => ({
|
|
231
|
+
cid: String(item?.cid || '').toLowerCase(),
|
|
232
|
+
agentId: String(item?.agentId || ''),
|
|
233
|
+
title: String(item?.title || '').trim(),
|
|
234
|
+
}))
|
|
235
|
+
.filter((item) => CONV_ID_RE.test(item.cid) && AGENT_ID_RE.test(item.agentId))
|
|
236
|
+
.slice(0, cap);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function startNewYuanbaoChat(page) {
|
|
240
|
+
await ensureYuanbaoPage(page);
|
|
241
|
+
if (await hasLoginGate(page)) return 'blocked';
|
|
242
|
+
const beforeUrl = await page.evaluate('window.location.href').catch(() => '');
|
|
243
|
+
const action = await page.evaluate(`(() => {
|
|
244
|
+
${IS_VISIBLE_JS}
|
|
245
|
+
const trigger = Array.from(document.querySelectorAll('.yb-common-nav__trigger[data-desc="new-chat"], [dt-button-id="new_temp_chat"]'))
|
|
246
|
+
.find((node) => isVisible(node));
|
|
247
|
+
if (trigger instanceof HTMLElement) {
|
|
248
|
+
trigger.click();
|
|
249
|
+
return 'clicked';
|
|
250
|
+
}
|
|
251
|
+
return 'navigate';
|
|
252
|
+
})()`);
|
|
253
|
+
if (action === 'navigate') {
|
|
254
|
+
await page.goto(YUANBAO_URL, { waitUntil: 'load', settleMs: 2500 });
|
|
255
|
+
await page.wait(1);
|
|
256
|
+
return (await hasLoginGate(page)) ? 'blocked' : 'navigate';
|
|
257
|
+
}
|
|
258
|
+
await page.wait(1);
|
|
259
|
+
if (await hasLoginGate(page)) return 'blocked';
|
|
260
|
+
const afterUrl = await page.evaluate('window.location.href').catch(() => '');
|
|
261
|
+
if (typeof afterUrl === 'string' && typeof beforeUrl === 'string' && afterUrl !== beforeUrl) {
|
|
262
|
+
return 'clicked';
|
|
263
|
+
}
|
|
264
|
+
// Click had no observable effect — fall back to homepage navigation.
|
|
265
|
+
await page.goto(YUANBAO_URL, { waitUntil: 'load', settleMs: 2500 });
|
|
266
|
+
await page.wait(1);
|
|
267
|
+
return 'navigate';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Drive the Quill composer to insert `prompt` and click the send button.
|
|
272
|
+
*
|
|
273
|
+
* Yuanbao toggles the send button between `style__send-btn___*` (enabled) and
|
|
274
|
+
* `style__send-btn--disabled___*` based on a debounced React re-render after
|
|
275
|
+
* the composer's input event. A naive 200ms wait races that re-render — the
|
|
276
|
+
* button is still `--disabled` at click time, the click is a no-op, and
|
|
277
|
+
* Enter-key fallback in Quill does not always submit. We poll the
|
|
278
|
+
* disabled-state for up to ~3s and only fall back to Enter when the React
|
|
279
|
+
* update never arrives.
|
|
280
|
+
*
|
|
281
|
+
* Returns `{ok: true, action}` or `{ok: false, reason, detail?}`.
|
|
282
|
+
*/
|
|
283
|
+
export async function sendYuanbaoMessage(page, prompt) {
|
|
284
|
+
return await page.evaluate(`(async () => {
|
|
285
|
+
const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
|
+
${IS_VISIBLE_JS}
|
|
287
|
+
|
|
288
|
+
const composer = Array.from(document.querySelectorAll('.ql-editor[contenteditable="true"], .ql-editor, [contenteditable="true"]'))
|
|
289
|
+
.find(isVisible);
|
|
290
|
+
|
|
291
|
+
if (!(composer instanceof HTMLElement)) {
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
reason: 'Yuanbao composer was not found.',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
composer.focus();
|
|
300
|
+
const selection = window.getSelection();
|
|
301
|
+
const range = document.createRange();
|
|
302
|
+
range.selectNodeContents(composer);
|
|
303
|
+
range.collapse(false);
|
|
304
|
+
selection?.removeAllRanges();
|
|
305
|
+
selection?.addRange(range);
|
|
306
|
+
composer.textContent = '';
|
|
307
|
+
document.execCommand('insertText', false, ${JSON.stringify(prompt)});
|
|
308
|
+
composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${JSON.stringify(prompt)}, inputType: 'insertText' }));
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
reason: 'Failed to insert the prompt into the Yuanbao composer.',
|
|
313
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const findEnabledSubmit = () => Array.from(document.querySelectorAll('a[class*="send-btn"], button[class*="send-btn"]'))
|
|
318
|
+
.find((node) => {
|
|
319
|
+
if (!(node instanceof HTMLElement) || !isVisible(node)) return false;
|
|
320
|
+
const className = typeof node.className === 'string' ? node.className : '';
|
|
321
|
+
return !className.includes('send-btn--disabled') && !className.includes('disabled');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
let submit = null;
|
|
325
|
+
const deadline = Date.now() + 3_000;
|
|
326
|
+
while (Date.now() < deadline) {
|
|
327
|
+
submit = findEnabledSubmit();
|
|
328
|
+
if (submit) break;
|
|
329
|
+
await waitFor(150);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (submit instanceof HTMLElement) {
|
|
333
|
+
submit.click();
|
|
334
|
+
return { ok: true, action: 'click' };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
338
|
+
composer.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
339
|
+
return { ok: true, action: 'enter' };
|
|
340
|
+
})()`);
|
|
341
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { normalizeBooleanFlag, parseYuanbaoSessionId } from './shared.js';
|
|
4
|
+
|
|
5
|
+
describe('yuanbao parseYuanbaoSessionId', () => {
|
|
6
|
+
const agentId = 'naQivTmsDa';
|
|
7
|
+
const convId = 'b1118732-15ca-42cc-bc9a-e40090ccfb8c';
|
|
8
|
+
|
|
9
|
+
it('extracts agent + conv from a full chat URL', () => {
|
|
10
|
+
expect(parseYuanbaoSessionId(`https://yuanbao.tencent.com/chat/${agentId}/${convId}`))
|
|
11
|
+
.toEqual({ agentId, convId });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('extracts agent + conv from URL with query string or fragment', () => {
|
|
15
|
+
expect(parseYuanbaoSessionId(`https://yuanbao.tencent.com/chat/${agentId}/${convId}?ref=share`))
|
|
16
|
+
.toEqual({ agentId, convId });
|
|
17
|
+
expect(parseYuanbaoSessionId(`https://yuanbao.tencent.com/chat/${agentId}/${convId}#anchor`))
|
|
18
|
+
.toEqual({ agentId, convId });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('lowercases the conv UUID', () => {
|
|
22
|
+
expect(parseYuanbaoSessionId(`https://yuanbao.tencent.com/chat/${agentId}/${convId.toUpperCase()}`))
|
|
23
|
+
.toEqual({ agentId, convId });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('accepts a bare "<agentId>/<convId>" pair', () => {
|
|
27
|
+
expect(parseYuanbaoSessionId(`${agentId}/${convId}`)).toEqual({ agentId, convId });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('throws on empty / blank input', () => {
|
|
31
|
+
expect(() => parseYuanbaoSessionId('')).toThrow(ArgumentError);
|
|
32
|
+
expect(() => parseYuanbaoSessionId(' ')).toThrow(ArgumentError);
|
|
33
|
+
expect(() => parseYuanbaoSessionId(null)).toThrow(ArgumentError);
|
|
34
|
+
expect(() => parseYuanbaoSessionId(undefined)).toThrow(ArgumentError);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('rejects a bare conv UUID (Yuanbao requires the agent slug)', () => {
|
|
38
|
+
expect(() => parseYuanbaoSessionId(convId)).toThrow(ArgumentError);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects a URL with a 37+ char conv tail (no silent truncation)', () => {
|
|
42
|
+
// Boundary regression: without `(?:[/?#]|$)` the regex would happily
|
|
43
|
+
// match the first 36 chars and silently open a different conversation.
|
|
44
|
+
expect(() => parseYuanbaoSessionId(`https://yuanbao.tencent.com/chat/${agentId}/${convId}extra`))
|
|
45
|
+
.toThrow(ArgumentError);
|
|
46
|
+
expect(() => parseYuanbaoSessionId(`https://yuanbao.tencent.com/chat/${agentId}/${convId}0000`))
|
|
47
|
+
.toThrow(ArgumentError);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('rejects malformed shapes with actionable messages', () => {
|
|
51
|
+
expect(() => parseYuanbaoSessionId('abc')).toThrow(ArgumentError);
|
|
52
|
+
expect(() => parseYuanbaoSessionId('https://yuanbao.tencent.com/somewhere/else')).toThrow(ArgumentError);
|
|
53
|
+
// agent slug too short to be valid
|
|
54
|
+
expect(() => parseYuanbaoSessionId(`abc/${convId}`)).toThrow(ArgumentError);
|
|
55
|
+
// conv part not a UUID
|
|
56
|
+
expect(() => parseYuanbaoSessionId(`${agentId}/not-a-uuid-at-all`)).toThrow(ArgumentError);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('yuanbao normalizeBooleanFlag', () => {
|
|
61
|
+
it('passes through real booleans', () => {
|
|
62
|
+
expect(normalizeBooleanFlag(true, false)).toBe(true);
|
|
63
|
+
expect(normalizeBooleanFlag(false, true)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns fallback for null/empty', () => {
|
|
67
|
+
expect(normalizeBooleanFlag(undefined, true)).toBe(true);
|
|
68
|
+
expect(normalizeBooleanFlag(null, false)).toBe(false);
|
|
69
|
+
expect(normalizeBooleanFlag('', true)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('parses common truthy / falsy strings', () => {
|
|
73
|
+
for (const v of ['true', 'TRUE', '1', 'yes', 'on']) {
|
|
74
|
+
expect(normalizeBooleanFlag(v, false)).toBe(true);
|
|
75
|
+
}
|
|
76
|
+
for (const v of ['false', '0', 'no', 'off', 'unknown']) {
|
|
77
|
+
expect(normalizeBooleanFlag(v, true)).toBe(false);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import {
|
|
3
|
+
YUANBAO_DOMAIN,
|
|
4
|
+
ensureYuanbaoPage,
|
|
5
|
+
isLoggedIn,
|
|
6
|
+
getCurrentYuanbaoSessionId,
|
|
7
|
+
getYuanbaoModelLabel,
|
|
8
|
+
} from './shared.js';
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'yuanbao',
|
|
12
|
+
name: 'status',
|
|
13
|
+
access: 'read',
|
|
14
|
+
description: 'Check Yuanbao page availability, login state, current session and model',
|
|
15
|
+
domain: YUANBAO_DOMAIN,
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
browser: true,
|
|
18
|
+
browserSession: { reuse: 'site' },
|
|
19
|
+
navigateBefore: false,
|
|
20
|
+
args: [],
|
|
21
|
+
columns: ['Status', 'Login', 'Model', 'ModelId', 'AgentId', 'SessionId', 'Url'],
|
|
22
|
+
func: async (page) => {
|
|
23
|
+
await ensureYuanbaoPage(page);
|
|
24
|
+
await page.wait(1.5);
|
|
25
|
+
const [loggedIn, session, model, url] = await Promise.all([
|
|
26
|
+
isLoggedIn(page),
|
|
27
|
+
getCurrentYuanbaoSessionId(page),
|
|
28
|
+
getYuanbaoModelLabel(page),
|
|
29
|
+
page.evaluate('window.location.href').catch(() => ''),
|
|
30
|
+
]);
|
|
31
|
+
// Surface unknown values as `null` (typed unknown) rather than '-' / ''
|
|
32
|
+
// sentinels — sentinels look like real labels and silently break filters
|
|
33
|
+
// built on these columns.
|
|
34
|
+
return [{
|
|
35
|
+
Status: 'Connected',
|
|
36
|
+
Login: loggedIn ? 'Yes' : 'No (login gate)',
|
|
37
|
+
Model: model.label,
|
|
38
|
+
ModelId: model.modelId,
|
|
39
|
+
AgentId: session?.agentId ?? null,
|
|
40
|
+
SessionId: session?.convId ?? null,
|
|
41
|
+
Url: typeof url === 'string' ? url : '',
|
|
42
|
+
}];
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -7,18 +7,8 @@ import {
|
|
|
7
7
|
} from './utils.js';
|
|
8
8
|
import './search.js';
|
|
9
9
|
import './info.js';
|
|
10
|
+
import { createPageMock } from '../test-utils.js';
|
|
10
11
|
|
|
11
|
-
function createPageMock(evaluateResults = []) {
|
|
12
|
-
const evaluate = vi.fn();
|
|
13
|
-
for (const result of evaluateResults) {
|
|
14
|
-
evaluate.mockResolvedValueOnce(result);
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
goto: vi.fn().mockResolvedValue(undefined),
|
|
18
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
19
|
-
evaluate,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
12
|
|
|
23
13
|
describe('zlibrary commands', () => {
|
|
24
14
|
it('registers search and info commands', () => {
|
|
@@ -19,6 +19,14 @@ export interface ResolveSuccess {
|
|
|
19
19
|
*/
|
|
20
20
|
match_level: TargetMatchLevel;
|
|
21
21
|
}
|
|
22
|
+
export interface FillTextResult extends ResolveSuccess {
|
|
23
|
+
filled: boolean;
|
|
24
|
+
verified: boolean;
|
|
25
|
+
expected: string;
|
|
26
|
+
actual: string;
|
|
27
|
+
length: number;
|
|
28
|
+
mode?: 'input' | 'textarea' | 'contenteditable';
|
|
29
|
+
}
|
|
22
30
|
export declare abstract class BasePage implements IPage {
|
|
23
31
|
protected _lastUrl: string | null;
|
|
24
32
|
/** Cached previous snapshot hashes for incremental diff marking */
|
|
@@ -63,6 +71,7 @@ export declare abstract class BasePage implements IPage {
|
|
|
63
71
|
*/
|
|
64
72
|
protected tryCdpOnResolvedElement(method: 'DOM.focus' | 'DOM.scrollIntoViewIfNeeded'): Promise<boolean>;
|
|
65
73
|
typeText(ref: string, text: string, opts?: ResolveOptions): Promise<ResolveSuccess>;
|
|
74
|
+
fillText(ref: string, text: string, opts?: ResolveOptions): Promise<FillTextResult>;
|
|
66
75
|
pressKey(key: string): Promise<void>;
|
|
67
76
|
scrollTo(ref: string, opts?: ResolveOptions): Promise<unknown>;
|
|
68
77
|
getFormState(): Promise<Record<string, unknown>>;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { generateSnapshotJs, getFormStateJs } from './dom-snapshot.js';
|
|
12
12
|
import { pressKeyJs, waitForTextJs, waitForCaptureJs, waitForSelectorJs, scrollJs, autoScrollJs, networkRequestsJs, waitForDomStableJs, } from './dom-helpers.js';
|
|
13
|
-
import { resolveTargetJs, clickResolvedJs, typeResolvedJs, prepareNativeTypeResolvedJs, scrollResolvedJs, } from './target-resolver.js';
|
|
13
|
+
import { resolveTargetJs, clickResolvedJs, typeResolvedJs, prepareNativeTypeResolvedJs, verifyFilledResolvedJs, scrollResolvedJs, } from './target-resolver.js';
|
|
14
14
|
import { TargetError } from './target-errors.js';
|
|
15
15
|
import { CliError } from '../errors.js';
|
|
16
16
|
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
@@ -303,6 +303,49 @@ export class BasePage {
|
|
|
303
303
|
}
|
|
304
304
|
return resolved;
|
|
305
305
|
}
|
|
306
|
+
async fillText(ref, text, opts = {}) {
|
|
307
|
+
const resolved = await runResolve(this, ref, opts);
|
|
308
|
+
let nativeScrolled = false;
|
|
309
|
+
let nativeFocused = false;
|
|
310
|
+
try {
|
|
311
|
+
nativeScrolled = await this.tryCdpOnResolvedElement('DOM.scrollIntoViewIfNeeded');
|
|
312
|
+
nativeFocused = await this.tryCdpOnResolvedElement('DOM.focus');
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// CDP focus/scroll is best-effort; DOM preparation below remains authoritative.
|
|
316
|
+
}
|
|
317
|
+
const preparation = await this.evaluate(prepareNativeTypeResolvedJs({
|
|
318
|
+
skipScroll: nativeScrolled,
|
|
319
|
+
skipFocus: nativeFocused,
|
|
320
|
+
}));
|
|
321
|
+
if (preparation?.ok !== true) {
|
|
322
|
+
throw new TargetError({
|
|
323
|
+
code: 'not_editable',
|
|
324
|
+
message: `Target "${ref}" is not a fillable input, textarea, or contenteditable element.`,
|
|
325
|
+
hint: 'Use `opencli browser state` to pick an editable target, or use `browser type` for keyboard-like interactions.',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const usedNativeInput = await this.tryNativeType(text);
|
|
329
|
+
if (!usedNativeInput) {
|
|
330
|
+
await this.evaluate(typeResolvedJs(text));
|
|
331
|
+
}
|
|
332
|
+
let verification = await this.evaluate(verifyFilledResolvedJs(text));
|
|
333
|
+
if (usedNativeInput && verification?.ok !== true) {
|
|
334
|
+
await this.evaluate(typeResolvedJs(text));
|
|
335
|
+
verification = await this.evaluate(verifyFilledResolvedJs(text));
|
|
336
|
+
}
|
|
337
|
+
const actual = verification && 'actual' in verification ? verification.actual : '';
|
|
338
|
+
const mode = verification && 'mode' in verification ? verification.mode : undefined;
|
|
339
|
+
return {
|
|
340
|
+
...resolved,
|
|
341
|
+
filled: true,
|
|
342
|
+
verified: verification?.ok === true,
|
|
343
|
+
expected: text,
|
|
344
|
+
actual,
|
|
345
|
+
length: actual.length,
|
|
346
|
+
...(mode ? { mode } : {}),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
306
349
|
async pressKey(key) {
|
|
307
350
|
const parsed = parseKeyChord(key);
|
|
308
351
|
if (!await this.tryNativeKeyPress(parsed.key, parsed.modifiers)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { CliError } from '../errors.js';
|
|
3
3
|
import { BasePage } from './base-page.js';
|
|
4
|
+
import { TargetError } from './target-errors.js';
|
|
4
5
|
class TestPage extends BasePage {
|
|
5
6
|
result;
|
|
6
7
|
args;
|
|
@@ -146,6 +147,71 @@ describe('BasePage native input routing', () => {
|
|
|
146
147
|
expect(page.scripts).toHaveLength(3);
|
|
147
148
|
expect(page.scripts[2]).toContain("return 'typed'");
|
|
148
149
|
});
|
|
150
|
+
it('fills text through the native input path and verifies the exact value', async () => {
|
|
151
|
+
const page = new ActionPage();
|
|
152
|
+
page.nativeType = vi.fn().mockResolvedValue(undefined);
|
|
153
|
+
page.results = [
|
|
154
|
+
resolveOk,
|
|
155
|
+
{ ok: true, mode: 'textarea' },
|
|
156
|
+
{ ok: true, actual: 'line1\\n/ / raw', expected: 'line1\\n/ / raw', length: 14, mode: 'textarea' },
|
|
157
|
+
];
|
|
158
|
+
await expect(page.fillText('#message', 'line1\\n/ / raw')).resolves.toEqual({
|
|
159
|
+
filled: true,
|
|
160
|
+
verified: true,
|
|
161
|
+
expected: 'line1\\n/ / raw',
|
|
162
|
+
actual: 'line1\\n/ / raw',
|
|
163
|
+
length: 14,
|
|
164
|
+
matches_n: 1,
|
|
165
|
+
match_level: 'exact',
|
|
166
|
+
mode: 'textarea',
|
|
167
|
+
});
|
|
168
|
+
expect(page.nativeType).toHaveBeenCalledWith('line1\\n/ / raw');
|
|
169
|
+
expect(page.scripts).toHaveLength(3);
|
|
170
|
+
expect(page.scripts[2]).toContain('actual ===');
|
|
171
|
+
});
|
|
172
|
+
it('falls back to the DOM setter when native fill insertion is unavailable', async () => {
|
|
173
|
+
const page = new ActionPage();
|
|
174
|
+
page.results = [
|
|
175
|
+
resolveOk,
|
|
176
|
+
{ ok: true, mode: 'input' },
|
|
177
|
+
'typed',
|
|
178
|
+
{ ok: true, actual: 'hello', expected: 'hello', length: 5, mode: 'input' },
|
|
179
|
+
];
|
|
180
|
+
await expect(page.fillText('#q', 'hello')).resolves.toEqual(expect.objectContaining({
|
|
181
|
+
filled: true,
|
|
182
|
+
verified: true,
|
|
183
|
+
actual: 'hello',
|
|
184
|
+
mode: 'input',
|
|
185
|
+
}));
|
|
186
|
+
expect(page.scripts).toHaveLength(4);
|
|
187
|
+
expect(page.scripts[2]).toContain("return 'typed'");
|
|
188
|
+
});
|
|
189
|
+
it('falls back to DOM fill if native insertion does not verify', async () => {
|
|
190
|
+
const page = new ActionPage();
|
|
191
|
+
page.nativeType = vi.fn().mockResolvedValue(undefined);
|
|
192
|
+
page.results = [
|
|
193
|
+
resolveOk,
|
|
194
|
+
{ ok: true, mode: 'input' },
|
|
195
|
+
{ ok: false, actual: '', expected: 'hello', length: 0, mode: 'input' },
|
|
196
|
+
'typed',
|
|
197
|
+
{ ok: true, actual: 'hello', expected: 'hello', length: 5, mode: 'input' },
|
|
198
|
+
];
|
|
199
|
+
await expect(page.fillText('#q', 'hello')).resolves.toEqual(expect.objectContaining({
|
|
200
|
+
filled: true,
|
|
201
|
+
verified: true,
|
|
202
|
+
actual: 'hello',
|
|
203
|
+
}));
|
|
204
|
+
expect(page.nativeType).toHaveBeenCalledWith('hello');
|
|
205
|
+
expect(page.scripts).toHaveLength(5);
|
|
206
|
+
expect(page.scripts[3]).toContain("return 'typed'");
|
|
207
|
+
});
|
|
208
|
+
it('throws a structured not_editable error for non-fillable targets', async () => {
|
|
209
|
+
const page = new ActionPage();
|
|
210
|
+
page.results = [resolveOk, { ok: false, reason: 'not_editable', tag: 'button' }];
|
|
211
|
+
const err = await page.fillText('button', 'hello').catch((error) => error);
|
|
212
|
+
expect(err).toBeInstanceOf(TargetError);
|
|
213
|
+
expect(err.code).toBe('not_editable');
|
|
214
|
+
});
|
|
149
215
|
it('uses CDP DOM scrollIntoViewIfNeeded before JS click when available', async () => {
|
|
150
216
|
const page = new ActionPage();
|
|
151
217
|
page.cdp = vi.fn()
|