@jackwener/opencli 1.7.12 → 1.7.13
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 +12194 -6843
- 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/reply-dm.js +1 -1
- package/clis/twitter/reply.test.js +1 -29
- 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/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/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 +150 -0
- package/dist/src/commanderAdapter.js +0 -5
- 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 +23 -2
- package/dist/src/help.js +41 -19
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +54 -0
- package/dist/src/main.js +14 -1
- package/dist/src/manifest-types.d.ts +5 -3
- package/dist/src/pipeline/executor.js +1 -1
- package/dist/src/pipeline/executor.test.js +8 -0
- package/dist/src/pipeline/registry.d.ts +9 -0
- package/dist/src/pipeline/registry.js +13 -1
- package/dist/src/pipeline/steps/browser.d.ts +1 -0
- package/dist/src/pipeline/steps/browser.js +10 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +12 -11
- package/dist/src/registry.js +16 -6
- package/dist/src/registry.test.js +2 -2
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/serialization.d.ts +2 -2
- package/dist/src/serialization.js +4 -6
- package/dist/src/serialization.test.js +17 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/validate.js +15 -11
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/package.json +1 -1
- package/scripts/fetch-adapters.js +1 -1
- package/scripts/typed-error-lint-baseline.json +5 -77
- package/clis/ctrip/search.test.js +0 -64
- package/clis/gov-policy/commands.test.js +0 -27
- package/clis/linux-do/category.js +0 -37
- package/clis/linux-do/hot.js +0 -26
- package/clis/linux-do/latest.js +0 -19
- package/clis/pixiv/test-utils.js +0 -23
- package/clis/toutiao/articles.test.js +0 -30
- package/dist/src/analysis.d.ts +0 -40
- package/dist/src/analysis.js +0 -172
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for ctrip public destination/hotel suggestion endpoints.
|
|
3
|
+
*
|
|
4
|
+
* The single backing endpoint `https://m.ctrip.com/restapi/soa2/21881/json/gaHotelSearchEngine`
|
|
5
|
+
* accepts a `searchType` discriminator:
|
|
6
|
+
* - `D` → destination suggest (cities, scenic spots, railway stations, landmarks)
|
|
7
|
+
* - `H` → hotel-context suggest (cities, business areas, individual hotels)
|
|
8
|
+
*
|
|
9
|
+
* Response shape is identical; we surface every field the endpoint emits as a
|
|
10
|
+
* stable column so callers do not silently lose geo / English / id metadata.
|
|
11
|
+
*/
|
|
12
|
+
import { ArgumentError, CliError } from '@jackwener/opencli/errors';
|
|
13
|
+
|
|
14
|
+
const ENDPOINT = 'https://m.ctrip.com/restapi/soa2/21881/json/gaHotelSearchEngine';
|
|
15
|
+
const MIN_LIMIT = 1;
|
|
16
|
+
const MAX_LIMIT = 50;
|
|
17
|
+
|
|
18
|
+
export function parseLimit(raw, fallback = 15) {
|
|
19
|
+
if (raw === undefined || raw === null || raw === '') return fallback;
|
|
20
|
+
const parsed = Number(raw);
|
|
21
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
|
22
|
+
throw new ArgumentError(`--limit must be an integer between ${MIN_LIMIT} and ${MAX_LIMIT}, got ${JSON.stringify(raw)}`);
|
|
23
|
+
}
|
|
24
|
+
if (parsed < MIN_LIMIT || parsed > MAX_LIMIT) {
|
|
25
|
+
throw new ArgumentError(`--limit must be between ${MIN_LIMIT} and ${MAX_LIMIT}, got ${parsed}`);
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchSuggest(query, searchType) {
|
|
31
|
+
let response;
|
|
32
|
+
try {
|
|
33
|
+
response = await fetch(ENDPOINT, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'content-type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
keyword: query,
|
|
38
|
+
searchType,
|
|
39
|
+
platform: 'online',
|
|
40
|
+
pageID: '102001',
|
|
41
|
+
head: {
|
|
42
|
+
Locale: 'zh-CN',
|
|
43
|
+
LocaleController: 'zh_cn',
|
|
44
|
+
Currency: 'CNY',
|
|
45
|
+
PageId: '102001',
|
|
46
|
+
clientID: 'opencli-ctrip',
|
|
47
|
+
group: 'ctrip',
|
|
48
|
+
Frontend: { sessionID: 1, pvid: 1 },
|
|
49
|
+
HotelExtension: { group: 'CTRIP', WebpSupport: false },
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new CliError(
|
|
55
|
+
'FETCH_ERROR',
|
|
56
|
+
`ctrip suggest fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
57
|
+
'Check your network connection and retry',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new CliError(
|
|
62
|
+
'FETCH_ERROR',
|
|
63
|
+
`ctrip suggest failed with status ${response.status}`,
|
|
64
|
+
'Retry the command or verify ctrip.com is reachable',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
let payload;
|
|
68
|
+
try {
|
|
69
|
+
payload = await response.json();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new CliError(
|
|
72
|
+
'COMMAND_EXEC',
|
|
73
|
+
`ctrip suggest returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
74
|
+
'Ctrip may have changed the endpoint response format; retry later',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (payload && payload.Result === false) {
|
|
78
|
+
const code = payload.ErrorCode ?? 'unknown';
|
|
79
|
+
throw new CliError(
|
|
80
|
+
'COMMAND_EXEC',
|
|
81
|
+
`ctrip suggest API returned Result=false (ErrorCode=${code})`,
|
|
82
|
+
'Verify keyword and retry; this typically means upstream rejected the query envelope',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return Array.isArray(payload?.Response?.searchResults) ? payload.Response.searchResults : [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Pick the best lat/lon pair available.
|
|
90
|
+
*
|
|
91
|
+
* Domestic Mainland China rows ship `gdLat`/`gdLon` (gaode); international rows
|
|
92
|
+
* ship `gLat`/`gLon` (google/wgs84). `lat`/`lon` is the legacy flat field — fall
|
|
93
|
+
* through to it last. Zero values are treated as "missing" since the endpoint
|
|
94
|
+
* uses 0.0 as a sentinel for unknown coords.
|
|
95
|
+
*/
|
|
96
|
+
export function pickCoords(item) {
|
|
97
|
+
const candidates = [
|
|
98
|
+
[item.gdLat, item.gdLon],
|
|
99
|
+
[item.gLat, item.gLon],
|
|
100
|
+
[item.lat, item.lon],
|
|
101
|
+
];
|
|
102
|
+
for (const [la, lo] of candidates) {
|
|
103
|
+
if (Number.isFinite(la) && Number.isFinite(lo) && (la !== 0 || lo !== 0)) {
|
|
104
|
+
return { lat: la, lon: lo };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { lat: null, lon: null };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a canonical user-facing URL from the suggest item type + ids.
|
|
112
|
+
* Unknown types return null (do not silently fabricate URLs).
|
|
113
|
+
*/
|
|
114
|
+
export function buildUrl(item) {
|
|
115
|
+
const id = item?.id ? String(item.id) : '';
|
|
116
|
+
const cityId = item?.cityId ?? '';
|
|
117
|
+
const cityName = item?.cityName ? String(item.cityName) : '';
|
|
118
|
+
switch (item?.type) {
|
|
119
|
+
case 'City':
|
|
120
|
+
return cityId ? `https://you.ctrip.com/place/${encodeURIComponent(cityName)}${cityId}.html` : null;
|
|
121
|
+
case 'Markland':
|
|
122
|
+
return id && cityId
|
|
123
|
+
? `https://you.ctrip.com/sight/${encodeURIComponent(cityName)}${cityId}/${id}.html`
|
|
124
|
+
: null;
|
|
125
|
+
case 'Hotel':
|
|
126
|
+
return id ? `https://hotels.ctrip.com/hotels/detail/?hotelid=${id}` : null;
|
|
127
|
+
case 'BusinessArea':
|
|
128
|
+
case 'Zone':
|
|
129
|
+
return cityId && id
|
|
130
|
+
? `https://hotels.ctrip.com/hotels/list?city=${cityId}&zone=${id}`
|
|
131
|
+
: null;
|
|
132
|
+
case 'RailwayStation':
|
|
133
|
+
return id ? `https://trains.ctrip.com/trainstation/${id}.html` : null;
|
|
134
|
+
default:
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function nz(v) {
|
|
140
|
+
return Number.isFinite(v) && v !== 0 ? v : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function firstNonZero(...values) {
|
|
144
|
+
for (const v of values) {
|
|
145
|
+
const n = Number(v);
|
|
146
|
+
if (Number.isFinite(n) && n !== 0) return n;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Project a raw suggest row into the stable adapter column shape.
|
|
153
|
+
* No silent fallbacks: every column has a deterministic value (string|number|null).
|
|
154
|
+
*/
|
|
155
|
+
export function mapSuggestRow(item, index) {
|
|
156
|
+
const { lat, lon } = pickCoords(item);
|
|
157
|
+
return {
|
|
158
|
+
rank: index + 1,
|
|
159
|
+
id: item?.id ? String(item.id) : null,
|
|
160
|
+
type: item?.type ? String(item.type) : null,
|
|
161
|
+
displayType: item?.displayType ? String(item.displayType).trim() : null,
|
|
162
|
+
name: String(item?.displayName || item?.word || item?.cityName || '').replace(/\s+/g, ' ').trim() || null,
|
|
163
|
+
eName: item?.eName ? String(item.eName).trim() : null,
|
|
164
|
+
cityId: Number.isFinite(item?.cityId) && item.cityId !== 0 ? item.cityId : null,
|
|
165
|
+
cityName: item?.cityName ? String(item.cityName).trim() : null,
|
|
166
|
+
provinceName: item?.provinceName ? String(item.provinceName).trim() : null,
|
|
167
|
+
countryName: item?.countryName ? String(item.countryName).trim() : null,
|
|
168
|
+
lat,
|
|
169
|
+
lon,
|
|
170
|
+
score: firstNonZero(item?.commentScore, item?.cStar),
|
|
171
|
+
url: buildUrl(item),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const __test__ = { ENDPOINT, MIN_LIMIT, MAX_LIMIT };
|
package/clis/cursor/ask.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import { selectorError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { ArgumentError, selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
export const askCommand = cli({
|
|
4
4
|
site: 'cursor',
|
|
5
5
|
name: 'ask',
|
|
@@ -10,12 +10,15 @@ export const askCommand = cli({
|
|
|
10
10
|
browser: true,
|
|
11
11
|
args: [
|
|
12
12
|
{ name: 'text', required: true, positional: true, help: 'Prompt to send' },
|
|
13
|
-
{ name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default:
|
|
13
|
+
{ name: 'timeout', type: 'int', required: false, help: 'Max seconds to wait for response (default: 30)', default: 30 },
|
|
14
14
|
],
|
|
15
15
|
columns: ['Role', 'Text'],
|
|
16
16
|
func: async (page, kwargs) => {
|
|
17
17
|
const text = kwargs.text;
|
|
18
|
-
const timeout =
|
|
18
|
+
const timeout = kwargs.timeout;
|
|
19
|
+
if (!Number.isInteger(timeout) || timeout < 1) {
|
|
20
|
+
throw new ArgumentError('--timeout must be a positive integer (seconds)');
|
|
21
|
+
}
|
|
19
22
|
// Count existing messages before sending
|
|
20
23
|
const beforeCount = await page.evaluate(`
|
|
21
24
|
document.querySelectorAll('[data-message-role]').length
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// dblp author — resolve an author name to a PID and list their publications
|
|
2
|
+
// newest first.
|
|
3
|
+
//
|
|
4
|
+
// Two-step lookup against dblp's public API:
|
|
5
|
+
// 1. `search/author/api?q=<name>` returns candidate authors (each with a
|
|
6
|
+
// stable PID URL like `https://dblp.org/pid/56/953`).
|
|
7
|
+
// 2. `pid/<pid>.xml` returns every publication under that PID, ordered
|
|
8
|
+
// newest first.
|
|
9
|
+
//
|
|
10
|
+
// We auto-resolve to the top hit. dblp's author search returns a single
|
|
11
|
+
// best-match for unique names; for ambiguous names (e.g. "Wei Wang") it
|
|
12
|
+
// returns multiple PIDs — we pick the highest-scored one and surface the
|
|
13
|
+
// resolved name + PID in the row metadata so the caller can refine.
|
|
14
|
+
//
|
|
15
|
+
// To bypass author search entirely, pass `--pid <prefix>/<id>` (e.g.
|
|
16
|
+
// `--pid 56/953`). This is the canonical disambiguator.
|
|
17
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
18
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
19
|
+
import {
|
|
20
|
+
DBLP_ORIGIN,
|
|
21
|
+
decodeXmlEntities,
|
|
22
|
+
dblpFetchJson,
|
|
23
|
+
dblpFetchXml,
|
|
24
|
+
extractRecordKey,
|
|
25
|
+
recordXmlToRow,
|
|
26
|
+
requireBoundedInt,
|
|
27
|
+
requireQuery,
|
|
28
|
+
} from './utils.js';
|
|
29
|
+
|
|
30
|
+
const PID_PATTERN = /^[0-9a-z]+(?:\/[0-9a-z-]+)+$/i;
|
|
31
|
+
|
|
32
|
+
function extractPidFromAuthorHit(hit) {
|
|
33
|
+
const url = String(hit?.info?.url ?? '').trim();
|
|
34
|
+
const m = url.match(/\/pid\/([^/]+(?:\/[^/]+)+)$/);
|
|
35
|
+
return m ? m[1] : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pickTopAuthor(hits) {
|
|
39
|
+
// dblp returns hits sorted by score desc; we just take the head.
|
|
40
|
+
return hits[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function splitRecords(xml) {
|
|
44
|
+
// Each publication is wrapped in <r>…</r> directly under <dblpperson>.
|
|
45
|
+
const out = [];
|
|
46
|
+
const re = /<r>\s*([\s\S]*?)\s*<\/r>/g;
|
|
47
|
+
let m;
|
|
48
|
+
while ((m = re.exec(String(xml || ''))) !== null) {
|
|
49
|
+
const inner = m[1];
|
|
50
|
+
// Skip cross-references that have no concrete record (rare).
|
|
51
|
+
if (/^<crossref/.test(inner)) continue;
|
|
52
|
+
out.push(inner);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
cli({
|
|
58
|
+
site: 'dblp',
|
|
59
|
+
name: 'author',
|
|
60
|
+
access: 'read',
|
|
61
|
+
description: 'List dblp publications by a given author (newest first; resolves to top PID match)',
|
|
62
|
+
domain: 'dblp.org',
|
|
63
|
+
strategy: Strategy.PUBLIC,
|
|
64
|
+
browser: false,
|
|
65
|
+
args: [
|
|
66
|
+
{ name: 'author', positional: true, required: false, help: 'Author name (e.g. "Yoshua Bengio"). Optional when --pid is given.' },
|
|
67
|
+
{ name: 'pid', help: 'Canonical dblp PID (e.g. "56/953"). Bypasses author search.' },
|
|
68
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max publications (1-200)' },
|
|
69
|
+
],
|
|
70
|
+
columns: ['rank', 'key', 'title', 'authors', 'venue', 'year', 'type', 'doi', 'pid', 'url'],
|
|
71
|
+
func: async (args) => {
|
|
72
|
+
const limit = requireBoundedInt(args.limit, 20, 200);
|
|
73
|
+
const pidArg = args.pid != null ? String(args.pid).trim() : '';
|
|
74
|
+
let pid = '';
|
|
75
|
+
let resolvedName = '';
|
|
76
|
+
if (pidArg) {
|
|
77
|
+
if (!PID_PATTERN.test(pidArg)) {
|
|
78
|
+
throw new ArgumentError(
|
|
79
|
+
`dblp pid "${pidArg}" is not a valid PID`,
|
|
80
|
+
'Expected something like "56/953" — visit the author page on dblp.org to find it.',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
pid = pidArg;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const name = requireQuery(args.author, 'author');
|
|
87
|
+
const json = await dblpFetchJson(
|
|
88
|
+
`/search/author/api?q=${encodeURIComponent(name)}&format=json&h=20`,
|
|
89
|
+
'dblp author search',
|
|
90
|
+
);
|
|
91
|
+
const raw = json?.result?.hits?.hit;
|
|
92
|
+
const hits = Array.isArray(raw) ? raw : (raw ? [raw] : []);
|
|
93
|
+
if (!hits.length) {
|
|
94
|
+
throw new EmptyResultError(
|
|
95
|
+
'dblp author',
|
|
96
|
+
`No dblp author matched "${name}". Try a different spelling, or pass --pid to bypass author search.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const top = pickTopAuthor(hits);
|
|
100
|
+
pid = extractPidFromAuthorHit(top);
|
|
101
|
+
if (!pid) {
|
|
102
|
+
throw new CommandExecutionError(
|
|
103
|
+
`dblp author search for "${name}" returned a hit without a PID URL`,
|
|
104
|
+
'dblp may have changed its author-search response shape; retry or pass --pid manually.',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
resolvedName = decodeXmlEntities(String(top?.info?.author ?? '')).trim();
|
|
108
|
+
}
|
|
109
|
+
const xml = await dblpFetchXml(`/pid/${pid}.xml`, `dblp pid ${pid}`);
|
|
110
|
+
const records = splitRecords(xml);
|
|
111
|
+
if (!records.length) {
|
|
112
|
+
throw new EmptyResultError(
|
|
113
|
+
'dblp author',
|
|
114
|
+
`dblp PID ${pid}${resolvedName ? ` (${resolvedName})` : ''} has no publications.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return records.slice(0, limit).map((recordXml, i) => {
|
|
118
|
+
const row = recordXmlToRow(`<root>${recordXml}</root>`);
|
|
119
|
+
return {
|
|
120
|
+
rank: i + 1,
|
|
121
|
+
key: row.key || extractRecordKey(recordXml),
|
|
122
|
+
title: row.title,
|
|
123
|
+
authors: row.authors,
|
|
124
|
+
venue: row.venue,
|
|
125
|
+
year: row.year,
|
|
126
|
+
type: row.type,
|
|
127
|
+
doi: row.doi,
|
|
128
|
+
pid,
|
|
129
|
+
url: row.open_access_url || row.dblp_url,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// dblp venue — search dblp's venue (conference / journal) registry.
|
|
2
|
+
//
|
|
3
|
+
// Hits `https://dblp.org/search/venue/api?q=…&format=json&h=…`. Returns a
|
|
4
|
+
// row per matched venue. Useful for resolving an acronym (e.g. "ICLR" →
|
|
5
|
+
// dblp's canonical venue page) and for browsing venues that match a topic.
|
|
6
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
8
|
+
import {
|
|
9
|
+
DBLP_ORIGIN,
|
|
10
|
+
decodeXmlEntities,
|
|
11
|
+
dblpFetchJson,
|
|
12
|
+
requireBoundedInt,
|
|
13
|
+
requireQuery,
|
|
14
|
+
} from './utils.js';
|
|
15
|
+
|
|
16
|
+
function simplifyVenueType(type) {
|
|
17
|
+
const t = String(type ?? '').trim();
|
|
18
|
+
if (!t) return '';
|
|
19
|
+
if (/Conference or Workshop/i.test(t)) return 'conf';
|
|
20
|
+
if (/Journal/i.test(t)) return 'journal';
|
|
21
|
+
if (/Series/i.test(t)) return 'series';
|
|
22
|
+
if (/Book/i.test(t)) return 'book';
|
|
23
|
+
if (/Reference/i.test(t)) return 'reference';
|
|
24
|
+
return t.toLowerCase().split(/\s+/)[0];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function venueHitToRow(hit, rank) {
|
|
28
|
+
const info = hit?.info ?? {};
|
|
29
|
+
const url = String(info.url ?? '').trim();
|
|
30
|
+
return {
|
|
31
|
+
rank,
|
|
32
|
+
acronym: String(info.acronym ?? '').trim(),
|
|
33
|
+
venue: decodeXmlEntities(info.venue ?? ''),
|
|
34
|
+
type: simplifyVenueType(info.type),
|
|
35
|
+
url: url.startsWith('http') ? url : url ? `${DBLP_ORIGIN}${url.startsWith('/') ? '' : '/'}${url}` : '',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
cli({
|
|
40
|
+
site: 'dblp',
|
|
41
|
+
name: 'venue',
|
|
42
|
+
access: 'read',
|
|
43
|
+
description: 'Search dblp venue registry (conferences / journals) by name or acronym',
|
|
44
|
+
domain: 'dblp.org',
|
|
45
|
+
strategy: Strategy.PUBLIC,
|
|
46
|
+
browser: false,
|
|
47
|
+
args: [
|
|
48
|
+
{ name: 'query', positional: true, required: true, help: 'Venue name or acronym (e.g. "ICLR", "neural networks")' },
|
|
49
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max venues (1-100, single dblp page)' },
|
|
50
|
+
],
|
|
51
|
+
columns: ['rank', 'acronym', 'venue', 'type', 'url'],
|
|
52
|
+
func: async (args) => {
|
|
53
|
+
const query = requireQuery(args.query);
|
|
54
|
+
const limit = requireBoundedInt(args.limit, 20, 100);
|
|
55
|
+
const path = `/search/venue/api?q=${encodeURIComponent(query)}&format=json&h=${limit}`;
|
|
56
|
+
const json = await dblpFetchJson(path, 'dblp venue');
|
|
57
|
+
const hits = json?.result?.hits?.hit;
|
|
58
|
+
const list = Array.isArray(hits) ? hits : [];
|
|
59
|
+
if (list.length === 0) {
|
|
60
|
+
throw new EmptyResultError('dblp venue', `No dblp venues matched "${query}".`);
|
|
61
|
+
}
|
|
62
|
+
return list.slice(0, limit).map((hit, i) => venueHitToRow(hit, i + 1));
|
|
63
|
+
},
|
|
64
|
+
});
|
package/clis/deepseek/ask.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CliError, CommandExecutionError, EXIT_CODES } from '@jackwener/opencli/
|
|
|
3
3
|
import {
|
|
4
4
|
DEEPSEEK_DOMAIN, DEEPSEEK_URL, ensureOnDeepSeek, selectModel, setFeature,
|
|
5
5
|
sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
|
|
6
|
+
pickResumeUrl,
|
|
6
7
|
} from './utils.js';
|
|
7
8
|
|
|
8
9
|
export const askCommand = cli({
|
|
@@ -13,8 +14,8 @@ export const askCommand = cli({
|
|
|
13
14
|
domain: DEEPSEEK_DOMAIN,
|
|
14
15
|
strategy: Strategy.COOKIE,
|
|
15
16
|
browser: true,
|
|
17
|
+
browserSession: { reuse: 'site' },
|
|
16
18
|
navigateBefore: false,
|
|
17
|
-
timeoutSeconds: 180,
|
|
18
19
|
args: [
|
|
19
20
|
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
|
|
20
21
|
{ name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
|
|
@@ -38,12 +39,16 @@ export const askCommand = cli({
|
|
|
38
39
|
} else {
|
|
39
40
|
const navigated = await ensureOnDeepSeek(page);
|
|
40
41
|
if (navigated) {
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
await page
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// Pinned conversations sit in their own DOM section and are
|
|
43
|
+
// skipped so the resume never lands on a topped chat.
|
|
44
|
+
const resumeUrl = await pickResumeUrl(page);
|
|
45
|
+
if (!resumeUrl) {
|
|
46
|
+
throw new CommandExecutionError(
|
|
47
|
+
'Workspace was recycled but no prior conversation could be loaded',
|
|
48
|
+
'Pass --new to start a fresh chat, or wait for the sidebar to populate before retrying.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
await page.goto(resumeUrl);
|
|
47
52
|
await page.wait(2);
|
|
48
53
|
}
|
|
49
54
|
}
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
mockWaitForResponse,
|
|
12
12
|
mockParseBoolFlag,
|
|
13
13
|
mockWithRetry,
|
|
14
|
+
mockPickResumeUrl,
|
|
14
15
|
} = vi.hoisted(() => ({
|
|
15
16
|
mockEnsureOnDeepSeek: vi.fn(),
|
|
16
17
|
mockSelectModel: vi.fn(),
|
|
@@ -21,6 +22,7 @@ const {
|
|
|
21
22
|
mockWaitForResponse: vi.fn(),
|
|
22
23
|
mockParseBoolFlag: vi.fn((v) => v === true || v === 'true'),
|
|
23
24
|
mockWithRetry: vi.fn(async (fn) => fn()),
|
|
25
|
+
mockPickResumeUrl: vi.fn(),
|
|
24
26
|
}));
|
|
25
27
|
|
|
26
28
|
vi.mock('./utils.js', () => ({
|
|
@@ -35,6 +37,7 @@ vi.mock('./utils.js', () => ({
|
|
|
35
37
|
waitForResponse: mockWaitForResponse,
|
|
36
38
|
parseBoolFlag: mockParseBoolFlag,
|
|
37
39
|
withRetry: mockWithRetry,
|
|
40
|
+
pickResumeUrl: mockPickResumeUrl,
|
|
38
41
|
}));
|
|
39
42
|
|
|
40
43
|
import { askCommand } from './ask.js';
|
|
@@ -185,9 +188,8 @@ describe('deepseek ask conversation resume', () => {
|
|
|
185
188
|
|
|
186
189
|
it('resumes the most recent conversation and skips model selection', async () => {
|
|
187
190
|
mockEnsureOnDeepSeek.mockResolvedValue(true);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// second evaluate: URL check (now inside a conversation)
|
|
191
|
+
mockPickResumeUrl.mockResolvedValue('https://chat.deepseek.com/a/chat/s/abc-123');
|
|
192
|
+
// URL check after resume navigation: now inside a conversation.
|
|
191
193
|
page.evaluate.mockResolvedValueOnce('https://chat.deepseek.com/a/chat/s/abc-123');
|
|
192
194
|
|
|
193
195
|
const rows = await askCommand.func(page, {
|
|
@@ -200,6 +202,7 @@ describe('deepseek ask conversation resume', () => {
|
|
|
200
202
|
});
|
|
201
203
|
|
|
202
204
|
expect(rows).toEqual([{ response: 'follow-up reply' }]);
|
|
205
|
+
expect(page.goto).toHaveBeenCalledWith('https://chat.deepseek.com/a/chat/s/abc-123');
|
|
203
206
|
expect(mockSelectModel).not.toHaveBeenCalled();
|
|
204
207
|
expect(mockSendMessage).toHaveBeenCalled();
|
|
205
208
|
});
|
|
@@ -243,25 +246,22 @@ describe('deepseek ask conversation resume', () => {
|
|
|
243
246
|
expect(mockSelectModel).not.toHaveBeenCalled();
|
|
244
247
|
});
|
|
245
248
|
|
|
246
|
-
it('
|
|
249
|
+
it('fails fast when the workspace was recycled but no conversation surfaces in time', async () => {
|
|
247
250
|
mockEnsureOnDeepSeek.mockResolvedValue(true);
|
|
248
|
-
|
|
249
|
-
// first evaluate: sidebar resume click (no link found)
|
|
250
|
-
page.evaluate.mockResolvedValueOnce(undefined);
|
|
251
|
-
// second evaluate: URL check (still on root page)
|
|
252
|
-
page.evaluate.mockResolvedValueOnce('https://chat.deepseek.com/');
|
|
251
|
+
mockPickResumeUrl.mockResolvedValue(null);
|
|
253
252
|
|
|
254
|
-
|
|
253
|
+
await expect(askCommand.func(page, {
|
|
255
254
|
prompt: 'hello',
|
|
256
255
|
timeout: 120,
|
|
257
256
|
new: false,
|
|
258
257
|
model: 'instant',
|
|
259
258
|
think: false,
|
|
260
259
|
search: false,
|
|
261
|
-
});
|
|
260
|
+
})).rejects.toBeInstanceOf(CommandExecutionError);
|
|
262
261
|
|
|
263
|
-
expect(
|
|
264
|
-
expect(mockSelectModel).toHaveBeenCalled();
|
|
262
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
263
|
+
expect(mockSelectModel).not.toHaveBeenCalled();
|
|
264
|
+
expect(mockSendMessage).not.toHaveBeenCalled();
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
it('skips search toggle in vision mode when search is not requested', async () => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import {
|
|
4
|
+
DEEPSEEK_DOMAIN,
|
|
5
|
+
ensureOnDeepSeek,
|
|
6
|
+
getVisibleMessages,
|
|
7
|
+
parseDeepSeekConversationId,
|
|
8
|
+
} from './utils.js';
|
|
9
|
+
|
|
10
|
+
export const detailCommand = cli({
|
|
11
|
+
site: 'deepseek',
|
|
12
|
+
name: 'detail',
|
|
13
|
+
access: 'read',
|
|
14
|
+
description: 'Read a specific DeepSeek conversation by ID',
|
|
15
|
+
domain: DEEPSEEK_DOMAIN,
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
browser: true,
|
|
18
|
+
browserSession: { reuse: 'site' },
|
|
19
|
+
navigateBefore: false,
|
|
20
|
+
args: [
|
|
21
|
+
{ name: 'id', required: true, positional: true, help: 'Conversation ID (UUID) or full /a/chat/s/<id> URL' },
|
|
22
|
+
],
|
|
23
|
+
columns: ['Role', 'Text'],
|
|
24
|
+
func: async (page, kwargs) => {
|
|
25
|
+
const id = parseDeepSeekConversationId(kwargs.id);
|
|
26
|
+
await ensureOnDeepSeek(page);
|
|
27
|
+
await page.goto(`https://chat.deepseek.com/a/chat/s/${id}`);
|
|
28
|
+
await page.wait(5);
|
|
29
|
+
const messages = await getVisibleMessages(page);
|
|
30
|
+
if (messages.length === 0) {
|
|
31
|
+
throw new EmptyResultError(
|
|
32
|
+
'deepseek detail',
|
|
33
|
+
`No visible messages found for conversation ${id}. Verify the ID is correct and that you are logged in.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return messages;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
mockEnsureOnDeepSeek,
|
|
7
|
+
mockGetVisibleMessages,
|
|
8
|
+
} = vi.hoisted(() => ({
|
|
9
|
+
mockEnsureOnDeepSeek: vi.fn(),
|
|
10
|
+
mockGetVisibleMessages: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('./utils.js', async () => {
|
|
14
|
+
const actual = await vi.importActual('./utils.js');
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
ensureOnDeepSeek: mockEnsureOnDeepSeek,
|
|
18
|
+
getVisibleMessages: mockGetVisibleMessages,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
import './detail.js';
|
|
23
|
+
|
|
24
|
+
describe('deepseek detail', () => {
|
|
25
|
+
const command = getRegistry().get('deepseek/detail');
|
|
26
|
+
const id = '749e6bbd-6a45-4440-beaa-ae5238bf06d8';
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('registers as a cookie-browser read command', () => {
|
|
34
|
+
expect(command).toBeDefined();
|
|
35
|
+
expect(command.browser).toBe(true);
|
|
36
|
+
expect(command.strategy).toBe('cookie');
|
|
37
|
+
expect(command.access).toBe('read');
|
|
38
|
+
expect(command.columns).toEqual(['Role', 'Text']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('navigates to the conversation URL and returns visible messages', async () => {
|
|
42
|
+
mockGetVisibleMessages.mockResolvedValue([
|
|
43
|
+
{ Role: 'user', Text: 'hello' },
|
|
44
|
+
{ Role: 'assistant', Text: 'hi' },
|
|
45
|
+
]);
|
|
46
|
+
const page = { wait: vi.fn().mockResolvedValue(undefined), goto: vi.fn().mockResolvedValue(undefined) };
|
|
47
|
+
|
|
48
|
+
const rows = await command.func(page, { id });
|
|
49
|
+
|
|
50
|
+
expect(rows).toEqual([
|
|
51
|
+
{ Role: 'user', Text: 'hello' },
|
|
52
|
+
{ Role: 'assistant', Text: 'hi' },
|
|
53
|
+
]);
|
|
54
|
+
expect(page.goto).toHaveBeenCalledWith(`https://chat.deepseek.com/a/chat/s/${id}`);
|
|
55
|
+
expect(mockGetVisibleMessages).toHaveBeenCalledWith(page);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('accepts a full chat URL and normalises it before navigation', async () => {
|
|
59
|
+
mockGetVisibleMessages.mockResolvedValue([{ Role: 'user', Text: 'hi' }]);
|
|
60
|
+
const page = { wait: vi.fn().mockResolvedValue(undefined), goto: vi.fn().mockResolvedValue(undefined) };
|
|
61
|
+
|
|
62
|
+
await command.func(page, { id: `https://chat.deepseek.com/a/chat/s/${id.toUpperCase()}?ref=foo` });
|
|
63
|
+
|
|
64
|
+
expect(page.goto).toHaveBeenCalledWith(`https://chat.deepseek.com/a/chat/s/${id}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rejects malformed IDs before browser navigation', async () => {
|
|
68
|
+
const page = { wait: vi.fn(), goto: vi.fn() };
|
|
69
|
+
|
|
70
|
+
await expect(command.func(page, { id: 'not-a-uuid' })).rejects.toThrow(ArgumentError);
|
|
71
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
72
|
+
expect(mockEnsureOnDeepSeek).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('throws EmptyResultError when the conversation has no visible messages', async () => {
|
|
76
|
+
mockGetVisibleMessages.mockResolvedValue([]);
|
|
77
|
+
const page = { wait: vi.fn().mockResolvedValue(undefined), goto: vi.fn().mockResolvedValue(undefined) };
|
|
78
|
+
|
|
79
|
+
await expect(command.func(page, { id })).rejects.toThrow(EmptyResultError);
|
|
80
|
+
});
|
|
81
|
+
});
|
package/clis/deepseek/history.js
CHANGED
|
@@ -9,6 +9,7 @@ export const historyCommand = cli({
|
|
|
9
9
|
domain: DEEPSEEK_DOMAIN,
|
|
10
10
|
strategy: Strategy.COOKIE,
|
|
11
11
|
browser: true,
|
|
12
|
+
browserSession: { reuse: 'site' },
|
|
12
13
|
navigateBefore: false,
|
|
13
14
|
args: [
|
|
14
15
|
{ name: 'limit', type: 'int', default: 20, help: 'Max conversations to show' },
|
package/clis/deepseek/new.js
CHANGED