@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,220 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
4
|
+
|
|
5
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
statSync: vi.fn((input) => {
|
|
10
|
+
const value = String(input);
|
|
11
|
+
if (value.includes('missing')) return undefined;
|
|
12
|
+
return { isFile: () => !value.includes('directory') };
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
vi.mock('node:path', async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal();
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
resolve: vi.fn((input) => `/abs/${input}`),
|
|
22
|
+
extname: vi.fn((input) => {
|
|
23
|
+
const match = String(input).match(/\.[^.]+$/);
|
|
24
|
+
return match ? match[0] : '';
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
import { __test__, publishCommand } from './publish.js';
|
|
30
|
+
|
|
31
|
+
function makePage({ evaluateResults = [], overrides = {} } = {}) {
|
|
32
|
+
const evaluate = vi.fn();
|
|
33
|
+
for (const result of evaluateResults) {
|
|
34
|
+
evaluate.mockResolvedValueOnce(result);
|
|
35
|
+
}
|
|
36
|
+
evaluate.mockResolvedValue({ ok: false, reason: 'unknown-state' });
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
41
|
+
evaluate,
|
|
42
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://www.goofish.com/publish'),
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runBrowserScript(html, script, { url = 'https://www.goofish.com/publish' } = {}) {
|
|
49
|
+
const dom = new JSDOM(html, { url, runScripts: 'outside-only' });
|
|
50
|
+
return dom.window.eval(script);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const validArgs = {
|
|
54
|
+
title: 'MacBook Pro',
|
|
55
|
+
description: '成色很好,功能正常',
|
|
56
|
+
price: '5999.99',
|
|
57
|
+
condition: '轻微使用',
|
|
58
|
+
category: '笔记本',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('xianyu/publish', () => {
|
|
62
|
+
it('builds the goofish publish URL', () => {
|
|
63
|
+
expect(__test__.buildPublishUrl()).toBe('https://www.goofish.com/publish');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('validates publish arguments before navigation', async () => {
|
|
67
|
+
const page = makePage();
|
|
68
|
+
|
|
69
|
+
await expect(publishCommand.func(page, { ...validArgs, title: ' ' })).rejects.toBeInstanceOf(ArgumentError);
|
|
70
|
+
await expect(publishCommand.func(page, { ...validArgs, price: '0' })).rejects.toBeInstanceOf(ArgumentError);
|
|
71
|
+
await expect(publishCommand.func(page, { ...validArgs, price: '12.345' })).rejects.toBeInstanceOf(ArgumentError);
|
|
72
|
+
await expect(publishCommand.func(page, { ...validArgs, condition: '八成新' })).rejects.toBeInstanceOf(ArgumentError);
|
|
73
|
+
await expect(publishCommand.func(page, { ...validArgs, images: 'a.bmp' })).rejects.toBeInstanceOf(ArgumentError);
|
|
74
|
+
await expect(publishCommand.func(page, { ...validArgs, images: 'missing.png' })).rejects.toBeInstanceOf(ArgumentError);
|
|
75
|
+
await expect(publishCommand.func(page, { ...validArgs, images: '1.png,2.png,3.png,4.png,5.png,6.png,7.png,8.png,9.png,10.png' })).rejects.toBeInstanceOf(ArgumentError);
|
|
76
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('publishes when every UI step has positive proof', async () => {
|
|
80
|
+
const page = makePage({
|
|
81
|
+
evaluateResults: [
|
|
82
|
+
{ hasPublishForm: true },
|
|
83
|
+
{ ok: true },
|
|
84
|
+
{ ok: true, filled: ['title', 'description', 'price', 'condition'], missing: [] },
|
|
85
|
+
{ ok: true },
|
|
86
|
+
{ status: 'published', item_id: '123456789012', url: 'https://www.goofish.com/item?id=123456789012' },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const rows = await publishCommand.func(page, validArgs);
|
|
91
|
+
|
|
92
|
+
expect(rows).toEqual([{
|
|
93
|
+
status: 'published',
|
|
94
|
+
item_id: '123456789012',
|
|
95
|
+
title: 'MacBook Pro',
|
|
96
|
+
price: '¥5999.99',
|
|
97
|
+
condition: '轻微使用',
|
|
98
|
+
url: 'https://www.goofish.com/item?id=123456789012',
|
|
99
|
+
message: '发布成功',
|
|
100
|
+
}]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('uses IPage getCurrentUrl instead of a non-existent page.url method', async () => {
|
|
104
|
+
const page = makePage({
|
|
105
|
+
evaluateResults: [
|
|
106
|
+
{ hasPublishForm: true },
|
|
107
|
+
{ ok: true },
|
|
108
|
+
{ ok: true, filled: ['title', 'description', 'price', 'condition'], missing: [] },
|
|
109
|
+
{ ok: true },
|
|
110
|
+
{ status: 'published', item_id: '123456789012' },
|
|
111
|
+
],
|
|
112
|
+
overrides: {
|
|
113
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://www.goofish.com/item?id=123456789012'),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(page.url).toBeUndefined();
|
|
118
|
+
|
|
119
|
+
const rows = await publishCommand.func(page, validArgs);
|
|
120
|
+
|
|
121
|
+
expect(page.getCurrentUrl).toHaveBeenCalled();
|
|
122
|
+
expect(rows[0].url).toBe('https://www.goofish.com/item?id=123456789012');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('maps login walls to AuthRequiredError', async () => {
|
|
126
|
+
const page = makePage({
|
|
127
|
+
evaluateResults: [
|
|
128
|
+
{ requiresAuth: true },
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await expect(publishCommand.func(page, validArgs)).rejects.toBeInstanceOf(AuthRequiredError);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('fails fast when category selection or form filling is not proven', async () => {
|
|
136
|
+
await expect(publishCommand.func(makePage({
|
|
137
|
+
evaluateResults: [
|
|
138
|
+
{ hasPublishForm: true },
|
|
139
|
+
{ ok: false, reason: 'category-not-found' },
|
|
140
|
+
],
|
|
141
|
+
}), validArgs)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
142
|
+
|
|
143
|
+
await expect(publishCommand.func(makePage({
|
|
144
|
+
evaluateResults: [
|
|
145
|
+
{ hasPublishForm: true },
|
|
146
|
+
{ ok: true },
|
|
147
|
+
{ ok: false, missing: ['price'] },
|
|
148
|
+
],
|
|
149
|
+
}), validArgs)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('uploads validated local images through the discovered file input', async () => {
|
|
153
|
+
const page = makePage({
|
|
154
|
+
evaluateResults: [
|
|
155
|
+
{ hasPublishForm: true },
|
|
156
|
+
{ ok: true },
|
|
157
|
+
{ ok: true, missing: [] },
|
|
158
|
+
{ ok: true, selector: '[id="upload"]' },
|
|
159
|
+
{ ok: true },
|
|
160
|
+
{ status: 'published', item_id: '123456789012', url: 'https://www.goofish.com/item?id=123456789012' },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await publishCommand.func(page, { ...validArgs, images: 'a.png,b.webp' });
|
|
165
|
+
|
|
166
|
+
expect(page.setFileInput).toHaveBeenCalledWith(['/abs/a.png', '/abs/b.webp'], '[id="upload"]');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('does not return a success row for failed or unconfirmed publish states', async () => {
|
|
170
|
+
await expect(publishCommand.func(makePage({
|
|
171
|
+
evaluateResults: [
|
|
172
|
+
{ hasPublishForm: true },
|
|
173
|
+
{ ok: true },
|
|
174
|
+
{ ok: true, missing: [] },
|
|
175
|
+
{ ok: true },
|
|
176
|
+
{ status: 'failed', message: '内容违规' },
|
|
177
|
+
],
|
|
178
|
+
}), validArgs)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
179
|
+
|
|
180
|
+
await expect(publishCommand.func(makePage({
|
|
181
|
+
evaluateResults: [
|
|
182
|
+
{ hasPublishForm: true },
|
|
183
|
+
{ ok: true },
|
|
184
|
+
{ ok: true, missing: [] },
|
|
185
|
+
{ ok: true },
|
|
186
|
+
],
|
|
187
|
+
}), validArgs)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('browser category script is async and returns typed failure reasons', async () => {
|
|
191
|
+
const result = await runBrowserScript('<main><button>其他</button></main>', __test__.buildSelectCategoryEvaluate('笔记本'));
|
|
192
|
+
|
|
193
|
+
expect(result).toEqual({ ok: false, reason: 'category-trigger-not-found' });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('browser fill script reports missing required fields', async () => {
|
|
197
|
+
const result = await runBrowserScript(`
|
|
198
|
+
<main>
|
|
199
|
+
<input placeholder="标题" />
|
|
200
|
+
<textarea id="desc"></textarea>
|
|
201
|
+
<button>轻微使用</button>
|
|
202
|
+
</main>
|
|
203
|
+
`, __test__.buildFillFormEvaluate(validArgs));
|
|
204
|
+
|
|
205
|
+
expect(result.ok).toBe(false);
|
|
206
|
+
expect(result.missing).toContain('price');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('browser success detector distinguishes success from failure and unknown states', async () => {
|
|
210
|
+
await expect(runBrowserScript('<body>发布成功</body>', __test__.buildDetectSuccessEvaluate(), {
|
|
211
|
+
url: 'https://www.goofish.com/item?id=123456789012',
|
|
212
|
+
})).resolves.toMatchObject({ status: 'published', item_id: '123456789012' });
|
|
213
|
+
|
|
214
|
+
await expect(runBrowserScript('<body><div class="error">内容违规</div></body>', __test__.buildDetectSuccessEvaluate()))
|
|
215
|
+
.resolves.toMatchObject({ status: 'failed', message: '内容违规' });
|
|
216
|
+
|
|
217
|
+
await expect(runBrowserScript('<body>处理中</body>', __test__.buildDetectSuccessEvaluate()))
|
|
218
|
+
.resolves.toEqual({ ok: false, reason: 'unknown-state' });
|
|
219
|
+
});
|
|
220
|
+
});
|
package/clis/xiaoe/catalog.js
CHANGED
|
@@ -1,19 +1,58 @@
|
|
|
1
|
+
// Xiaoe (小鹅通) catalog — list chapters + sections of a course / column
|
|
2
|
+
// page (`h5.xet.citv.cn`).
|
|
3
|
+
//
|
|
4
|
+
// Replaces the legacy `pipeline:[]` form. The in-browser extraction logic
|
|
5
|
+
// (Vue store walking + auto-scroll-to-load + Vue child traversal) is kept
|
|
6
|
+
// byte-for-byte — Xiaoe's pages are SPA-rendered and Vue's private API
|
|
7
|
+
// (`__vue__`, `$store`, `$children`, `chapter_box.__vue__`) is the only
|
|
8
|
+
// stable hook we have. JSDOM cannot reproduce the Vue runtime tree, so
|
|
9
|
+
// reorganising the IIFE without live verify would be silent-failure risk.
|
|
10
|
+
//
|
|
11
|
+
// What changes:
|
|
12
|
+
// - `func` form + `Strategy.COOKIE` + `browser:true`.
|
|
13
|
+
// - Typed errors: `ArgumentError` on missing url; `EmptyResultError`
|
|
14
|
+
// when the IIFE yields zero rows (almost always means the cookie
|
|
15
|
+
// expired or the URL is not a course page); `CommandExecutionError`
|
|
16
|
+
// when `page.evaluate` rejects.
|
|
17
|
+
// - Three pure helpers (`typeLabel`, `buildItemUrl`, `chapterUrlPath`)
|
|
18
|
+
// are module-level exports and are embedded into the in-page IIFE
|
|
19
|
+
// via `${fn.toString()}` so the live and the test path share one
|
|
20
|
+
// source of truth.
|
|
21
|
+
|
|
1
22
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
24
|
+
import { requireXiaoePageUrl } from './content.js';
|
|
25
|
+
|
|
26
|
+
// resource_type → human label. 1=图文 2=直播 3=音频 4=视频 6=专栏 8=大专栏.
|
|
27
|
+
// Returns the raw `String(t)` when the type is unknown (e.g. xiaoe rolls
|
|
28
|
+
// out a new resource type) — never silently swallows it.
|
|
29
|
+
export function typeLabel(t) {
|
|
30
|
+
const map = { 1: '图文', 2: '直播', 3: '音频', 4: '视频', 6: '专栏', 8: '大专栏' };
|
|
31
|
+
return map[Number(t)] || String(t || '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Resolve a relative `jump_url` / `h5_url` / `url` against the page's
|
|
35
|
+
// origin. Returns '' when the item has no URL field at all.
|
|
36
|
+
export function buildItemUrl(item, origin) {
|
|
37
|
+
const u = item.jump_url || item.h5_url || item.url || '';
|
|
38
|
+
if (!u) return '';
|
|
39
|
+
return u.startsWith('http') ? u : (origin + u);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// chapter_type → URL path for the section reader. Xiaoe routes chapter
|
|
43
|
+
// types to different player paths; returning `undefined` for unknown
|
|
44
|
+
// types lets the caller decide whether to emit `''` instead of guessing
|
|
45
|
+
// a bad URL.
|
|
46
|
+
export function chapterUrlPath(chType) {
|
|
47
|
+
const map = { 1: '/v1/course/text/', 2: '/v2/course/alive/', 3: '/v1/course/audio/', 4: '/v1/course/video/' };
|
|
48
|
+
return map[Number(chType)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildCatalogScript() {
|
|
52
|
+
return `(async () => {
|
|
53
|
+
${typeLabel.toString()}
|
|
54
|
+
${buildItemUrl.toString()}
|
|
55
|
+
${chapterUrlPath.toString()}
|
|
17
56
|
var el = document.querySelector('#app');
|
|
18
57
|
var store = (el && el.__vue__) ? el.__vue__.$store : null;
|
|
19
58
|
if (!store) return [];
|
|
@@ -22,13 +61,6 @@ cli({
|
|
|
22
61
|
var origin = window.location.origin;
|
|
23
62
|
var courseName = coreInfo.resource_name || '';
|
|
24
63
|
|
|
25
|
-
function typeLabel(t) {
|
|
26
|
-
return {1:'图文',2:'直播',3:'音频',4:'视频',6:'专栏',8:'大专栏'}[Number(t)] || String(t||'');
|
|
27
|
-
}
|
|
28
|
-
function buildUrl(item) {
|
|
29
|
-
var u = item.jump_url || item.h5_url || item.url || '';
|
|
30
|
-
return (u && !u.startsWith('http')) ? origin + u : u;
|
|
31
|
-
}
|
|
32
64
|
function clickTab(name) {
|
|
33
65
|
var tabs = document.querySelectorAll('span, div');
|
|
34
66
|
for (var i = 0; i < tabs.length; i++) {
|
|
@@ -61,7 +93,7 @@ cli({
|
|
|
61
93
|
if(scrollers[si].scrollHeight > scrollers[si].clientHeight) scrollers[si].scrollTop = scrollers[si].scrollHeight;
|
|
62
94
|
}
|
|
63
95
|
await new Promise(function(r) { setTimeout(r, 800); });
|
|
64
|
-
|
|
96
|
+
|
|
65
97
|
// 点击可能存在的下拉/加载更多
|
|
66
98
|
var moreTabs = document.querySelectorAll('span, div, p');
|
|
67
99
|
for (var bi = 0; bi < moreTabs.length; bi++) {
|
|
@@ -70,7 +102,7 @@ cli({
|
|
|
70
102
|
try { moreTabs[bi].click(); } catch(e){}
|
|
71
103
|
}
|
|
72
104
|
}
|
|
73
|
-
|
|
105
|
+
|
|
74
106
|
var maxScrollHeight = getMaxScrollHeight(getScrollTargets());
|
|
75
107
|
if (sc > 3 && maxScrollHeight === prevMaxScrollHeight) break;
|
|
76
108
|
prevMaxScrollHeight = maxScrollHeight;
|
|
@@ -92,11 +124,13 @@ cli({
|
|
|
92
124
|
var item = arr[j];
|
|
93
125
|
if (!item.resource_id || !/^[pvlai]_/.test(item.resource_id)) continue;
|
|
94
126
|
listData.push({
|
|
95
|
-
ch: 1,
|
|
127
|
+
ch: 1,
|
|
128
|
+
chapter: courseName,
|
|
129
|
+
no: j + 1,
|
|
96
130
|
title: item.resource_title || item.title || item.chapter_title || '',
|
|
97
131
|
type: typeLabel(item.resource_type || item.chapter_type),
|
|
98
132
|
resource_id: item.resource_id,
|
|
99
|
-
url:
|
|
133
|
+
url: buildItemUrl(item, origin),
|
|
100
134
|
status: item.finished_state === 1 ? '已完成' : (item.resource_count ? item.resource_count + '节' : ''),
|
|
101
135
|
});
|
|
102
136
|
}
|
|
@@ -134,9 +168,11 @@ cli({
|
|
|
134
168
|
var child = children[ck];
|
|
135
169
|
var resId = child.resource_id || child.chapter_id || '';
|
|
136
170
|
var chType = child.chapter_type || child.resource_type || 0;
|
|
137
|
-
var urlPath =
|
|
171
|
+
var urlPath = chapterUrlPath(chType);
|
|
138
172
|
result.push({
|
|
139
|
-
ch: cj + 1,
|
|
173
|
+
ch: cj + 1,
|
|
174
|
+
chapter: chTitle,
|
|
175
|
+
no: ck + 1,
|
|
140
176
|
title: child.chapter_title || child.resource_title || '',
|
|
141
177
|
type: typeLabel(chType),
|
|
142
178
|
resource_id: resId,
|
|
@@ -146,17 +182,46 @@ cli({
|
|
|
146
182
|
}
|
|
147
183
|
}
|
|
148
184
|
return result;
|
|
149
|
-
})()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
})()`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function getXiaoeCatalog(page, args) {
|
|
189
|
+
const url = requireXiaoePageUrl(args.url, 'catalog');
|
|
190
|
+
let rows;
|
|
191
|
+
try {
|
|
192
|
+
await page.goto(url, { waitUntil: 'load', settleMs: 8000 });
|
|
193
|
+
rows = await page.evaluate(buildCatalogScript());
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
throw new CommandExecutionError(
|
|
197
|
+
`Failed to read xiaoe catalog: ${message}`,
|
|
198
|
+
'page may not have rendered or auth may be required',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
202
|
+
throw new EmptyResultError(
|
|
203
|
+
'xiaoe/catalog',
|
|
204
|
+
'No catalog rows extracted — the URL may not be a course page or the login session has expired',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return rows;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const catalogCommand = cli({
|
|
211
|
+
site: 'xiaoe',
|
|
212
|
+
name: 'catalog',
|
|
213
|
+
access: 'read',
|
|
214
|
+
description: '小鹅通课程目录(支持普通课程、专栏、大专栏)',
|
|
215
|
+
domain: 'h5.xet.citv.cn',
|
|
216
|
+
strategy: Strategy.COOKIE,
|
|
217
|
+
browser: true,
|
|
218
|
+
args: [
|
|
219
|
+
{ name: 'url', required: true, positional: true, help: '课程页面 URL' },
|
|
161
220
|
],
|
|
221
|
+
columns: ['ch', 'chapter', 'no', 'title', 'type', 'resource_id', 'url', 'status'],
|
|
222
|
+
func: getXiaoeCatalog,
|
|
162
223
|
});
|
|
224
|
+
|
|
225
|
+
export const __test__ = {
|
|
226
|
+
buildCatalogScript,
|
|
227
|
+
};
|
package/clis/xiaoe/content.js
CHANGED
|
@@ -1,40 +1,175 @@
|
|
|
1
|
+
// Xiaoe (小鹅通) content extractor — pulls rendered article text from a
|
|
2
|
+
// rich-text page (h5.xet.citv.cn).
|
|
3
|
+
//
|
|
4
|
+
// Replaces the legacy `pipeline:[]` form. Two real bugs in the legacy
|
|
5
|
+
// adapter, both silent:
|
|
6
|
+
// 1. The IIFE returned `{title, content, content_length, image_count,
|
|
7
|
+
// images}` but `columns` declared `[title, content_length,
|
|
8
|
+
// image_count]` — `content` (the text the adapter exists to
|
|
9
|
+
// extract!) and `images` were silently dropped before reaching the
|
|
10
|
+
// caller. The user got "this article has 1234 chars" with no way
|
|
11
|
+
// to read those chars.
|
|
12
|
+
// 2. `JSON.stringify(images.slice(0, 20))` silently truncated to the
|
|
13
|
+
// first 20 image URLs and never told the caller the slice
|
|
14
|
+
// happened.
|
|
15
|
+
//
|
|
16
|
+
// New behavior:
|
|
17
|
+
// - `func` form + `Strategy.COOKIE` + `browser:true` (the page is
|
|
18
|
+
// gated behind a logged-in xiaoe session).
|
|
19
|
+
// - Pure helpers `pickContentText` / `countXiaoeImages` are
|
|
20
|
+
// module-level exports; the in-page IIFE embeds them via
|
|
21
|
+
// `${fn.toString()}` while JSDOM tests call the same exports
|
|
22
|
+
// directly against a hand-crafted fixture (same pattern as
|
|
23
|
+
// dianping #1313 / hupu #1387).
|
|
24
|
+
// - `content` is now a real column (the bug fix). `image_count` is
|
|
25
|
+
// metadata that helps callers decide whether to re-render the
|
|
26
|
+
// page for a JSON-image dump in a follow-up adapter.
|
|
27
|
+
// - Empty-content extraction → `EmptyResultError` with a
|
|
28
|
+
// login-likely hint (xiaoe routinely renders an empty shell when
|
|
29
|
+
// the cookie has expired). No silent `return [{ content: '' }]`.
|
|
30
|
+
|
|
1
31
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
|
|
32
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
33
|
+
|
|
34
|
+
export const CONTENT_SELECTORS = [
|
|
35
|
+
'.rich-text-wrap',
|
|
36
|
+
'.content-wrap',
|
|
37
|
+
'.article-content',
|
|
38
|
+
'.text-content',
|
|
39
|
+
'.course-detail',
|
|
40
|
+
'.detail-content',
|
|
41
|
+
'[class*="richtext"]',
|
|
42
|
+
'[class*="rich-text"]',
|
|
43
|
+
'.ql-editor',
|
|
44
|
+
];
|
|
45
|
+
export const CONTENT_MIN_LENGTH = 50;
|
|
46
|
+
|
|
47
|
+
// Pure: walk `selectors` in order, return the trimmed text of the first
|
|
48
|
+
// element whose `.innerText` (with `.textContent` fallback for JSDOM)
|
|
49
|
+
// has more than `minLength` characters. Falls back to `<main>`, then
|
|
50
|
+
// `#app`, then `<body>` — same chain the legacy IIFE used.
|
|
51
|
+
export function pickContentText(doc, selectors, minLength = CONTENT_MIN_LENGTH) {
|
|
52
|
+
for (const sel of selectors) {
|
|
53
|
+
const el = doc.querySelector(sel);
|
|
54
|
+
if (!el) continue;
|
|
55
|
+
const text = ((el.innerText || el.textContent) || '').trim();
|
|
56
|
+
if (text.length > minLength) return text;
|
|
57
|
+
}
|
|
58
|
+
const fallback = doc.querySelector('main')
|
|
59
|
+
|| doc.querySelector('#app')
|
|
60
|
+
|| doc.body;
|
|
61
|
+
if (!fallback) return '';
|
|
62
|
+
return ((fallback.innerText || fallback.textContent) || '').trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Pure: count `<img>` elements whose src looks like a real xiaoe-hosted
|
|
66
|
+
// asset. `data:` URIs and non-xiaoe CDNs (avatars, ads) are excluded.
|
|
67
|
+
export function countXiaoeImages(doc) {
|
|
68
|
+
let count = 0;
|
|
69
|
+
const imgs = doc.querySelectorAll('img');
|
|
70
|
+
for (let i = 0; i < imgs.length; i += 1) {
|
|
71
|
+
const src = imgs[i].getAttribute('src') || imgs[i].src || '';
|
|
72
|
+
if (!src) continue;
|
|
73
|
+
if (src.startsWith('data:')) continue;
|
|
74
|
+
if (!src.includes('xiaoe')) continue;
|
|
75
|
+
count += 1;
|
|
76
|
+
}
|
|
77
|
+
return count;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function requireXiaoePageUrl(value, commandName) {
|
|
81
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
82
|
+
if (!raw) {
|
|
83
|
+
throw new ArgumentError('url is required (positional)');
|
|
84
|
+
}
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = new URL(raw);
|
|
88
|
+
} catch {
|
|
89
|
+
throw new ArgumentError(
|
|
90
|
+
`invalid xiaoe URL: ${raw}`,
|
|
91
|
+
`Example: opencli xiaoe ${commandName} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (parsed.protocol !== 'https:') {
|
|
95
|
+
throw new ArgumentError(
|
|
96
|
+
`xiaoe URL must use https (got ${parsed.protocol.replace(':', '')})`,
|
|
97
|
+
`Example: opencli xiaoe ${commandName} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const host = parsed.hostname.toLowerCase();
|
|
101
|
+
if (host !== 'h5.xet.citv.cn' && !host.endsWith('.h5.xet.citv.cn')) {
|
|
102
|
+
throw new ArgumentError(
|
|
103
|
+
`url must be on h5.xet.citv.cn or a shop subdomain (got ${parsed.hostname})`,
|
|
104
|
+
`Example: opencli xiaoe ${commandName} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return parsed.toString();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildContentScript() {
|
|
111
|
+
return `
|
|
112
|
+
(() => {
|
|
113
|
+
${pickContentText.toString()}
|
|
114
|
+
${countXiaoeImages.toString()}
|
|
115
|
+
const selectors = ${JSON.stringify(CONTENT_SELECTORS)};
|
|
116
|
+
const title = document.title || '';
|
|
117
|
+
const content = pickContentText(document, selectors, ${JSON.stringify(CONTENT_MIN_LENGTH)});
|
|
118
|
+
const imageCount = countXiaoeImages(document);
|
|
119
|
+
return [{
|
|
120
|
+
title,
|
|
121
|
+
content,
|
|
122
|
+
content_length: content.length,
|
|
123
|
+
image_count: imageCount,
|
|
124
|
+
}];
|
|
125
|
+
})()
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function getXiaoeContent(page, args) {
|
|
130
|
+
const url = requireXiaoePageUrl(args.url, 'content');
|
|
131
|
+
let rows;
|
|
132
|
+
try {
|
|
133
|
+
await page.goto(url, { waitUntil: 'load', settleMs: 6000 });
|
|
134
|
+
rows = await page.evaluate(buildContentScript());
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
throw new CommandExecutionError(
|
|
138
|
+
`Failed to extract xiaoe content: ${message}`,
|
|
139
|
+
'page may not have rendered or auth may be required',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
143
|
+
throw new EmptyResultError(
|
|
144
|
+
'xiaoe/content',
|
|
145
|
+
'No rows returned from page evaluator (page structure may have changed)',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const row = rows[0];
|
|
149
|
+
if (!row || typeof row.content !== 'string' || row.content.length === 0) {
|
|
150
|
+
throw new EmptyResultError(
|
|
151
|
+
'xiaoe/content',
|
|
152
|
+
'No article content extracted — login session may have expired or the page renders an empty shell',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const contentCommand = cli({
|
|
3
159
|
site: 'xiaoe',
|
|
4
160
|
name: 'content',
|
|
5
161
|
access: 'read',
|
|
6
162
|
description: '提取小鹅通图文页面内容为文本',
|
|
7
163
|
domain: 'h5.xet.citv.cn',
|
|
8
164
|
strategy: Strategy.COOKIE,
|
|
165
|
+
browser: true,
|
|
9
166
|
args: [
|
|
10
167
|
{ name: 'url', required: true, positional: true, help: '页面 URL' },
|
|
11
168
|
],
|
|
12
|
-
columns: ['title', 'content_length', 'image_count'],
|
|
13
|
-
|
|
14
|
-
{ navigate: '${{ args.url }}' },
|
|
15
|
-
{ wait: 6 },
|
|
16
|
-
{ evaluate: `(() => {
|
|
17
|
-
var selectors = ['.rich-text-wrap','.content-wrap','.article-content','.text-content',
|
|
18
|
-
'.course-detail','.detail-content','[class*="richtext"]','[class*="rich-text"]','.ql-editor'];
|
|
19
|
-
var content = '';
|
|
20
|
-
for (var i = 0; i < selectors.length; i++) {
|
|
21
|
-
var el = document.querySelector(selectors[i]);
|
|
22
|
-
if (el && el.innerText.trim().length > 50) { content = el.innerText.trim(); break; }
|
|
23
|
-
}
|
|
24
|
-
if (!content) content = (document.querySelector('main') || document.querySelector('#app') || document.body).innerText.trim();
|
|
25
|
-
|
|
26
|
-
var images = [];
|
|
27
|
-
document.querySelectorAll('img').forEach(function(img) {
|
|
28
|
-
if (img.src && !img.src.startsWith('data:') && img.src.includes('xiaoe')) images.push(img.src);
|
|
29
|
-
});
|
|
30
|
-
return [{
|
|
31
|
-
title: document.title,
|
|
32
|
-
content: content,
|
|
33
|
-
content_length: content.length,
|
|
34
|
-
image_count: images.length,
|
|
35
|
-
images: JSON.stringify(images.slice(0, 20)),
|
|
36
|
-
}];
|
|
37
|
-
})()
|
|
38
|
-
` },
|
|
39
|
-
],
|
|
169
|
+
columns: ['title', 'content', 'content_length', 'image_count'],
|
|
170
|
+
func: getXiaoeContent,
|
|
40
171
|
});
|
|
172
|
+
|
|
173
|
+
export const __test__ = {
|
|
174
|
+
buildContentScript,
|
|
175
|
+
};
|