@jackwener/opencli 1.6.0 → 1.6.2
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/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -45
- package/README.zh-CN.md +32 -34
- package/autoresearch/browse-tasks.json +18 -20
- package/autoresearch/commands/debug.ts +163 -0
- package/autoresearch/commands/fix.ts +145 -0
- package/autoresearch/commands/plan.ts +88 -0
- package/autoresearch/commands/run.ts +138 -0
- package/autoresearch/config.ts +82 -0
- package/autoresearch/engine.ts +359 -0
- package/autoresearch/eval-all.ts +127 -0
- package/autoresearch/eval-browse.ts +1 -1
- package/autoresearch/eval-publish.ts +238 -0
- package/autoresearch/eval-save.ts +249 -0
- package/autoresearch/eval-skill.ts +14 -8
- package/autoresearch/eval-v2ex.ts +220 -0
- package/autoresearch/eval-zhihu.ts +230 -0
- package/autoresearch/logger.ts +69 -0
- package/autoresearch/presets/combined-reliability.ts +27 -0
- package/autoresearch/presets/index.ts +23 -0
- package/autoresearch/presets/operate-reliability.ts +24 -0
- package/autoresearch/presets/save-reliability.ts +26 -0
- package/autoresearch/presets/skill-quality.ts +20 -0
- package/autoresearch/presets/v2ex-reliability.ts +24 -0
- package/autoresearch/presets/zhihu-reliability.ts +25 -0
- package/autoresearch/publish-tasks.json +345 -0
- package/autoresearch/run-save.sh +11 -0
- package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
- package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
- package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
- package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
- package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
- package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
- package/autoresearch/save-tasks.json +281 -0
- package/autoresearch/v2ex-tasks.json +899 -0
- package/autoresearch/zhihu-tasks.json +848 -0
- package/bun.lock +615 -0
- package/dist/browser/base-page.d.ts +4 -2
- package/dist/browser/base-page.js +37 -4
- package/dist/browser/bridge.js +10 -8
- package/dist/browser/cdp.js +2 -6
- package/dist/browser/daemon-client.d.ts +11 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.d.ts +4 -2
- package/dist/browser/dom-helpers.js +42 -31
- package/dist/browser/dom-snapshot.js +23 -1
- package/dist/browser/page.d.ts +7 -2
- package/dist/browser/page.js +112 -30
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +1 -0
- package/dist/cli-manifest.json +1133 -182
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +48 -7
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +88 -0
- package/dist/clis/1688/item.d.ts +70 -0
- package/dist/clis/1688/item.js +187 -0
- package/dist/clis/1688/item.test.d.ts +1 -0
- package/dist/clis/1688/item.test.js +67 -0
- package/dist/clis/1688/search.d.ts +56 -0
- package/dist/clis/1688/search.js +309 -0
- package/dist/clis/1688/search.test.d.ts +1 -0
- package/dist/clis/1688/search.test.js +75 -0
- package/dist/clis/1688/shared.d.ts +112 -0
- package/dist/clis/1688/shared.js +514 -0
- package/dist/clis/1688/shared.test.d.ts +1 -0
- package/dist/clis/1688/shared.test.js +57 -0
- package/dist/clis/1688/store.d.ts +45 -0
- package/dist/clis/1688/store.js +226 -0
- package/dist/clis/1688/store.test.d.ts +1 -0
- package/dist/clis/1688/store.test.js +62 -0
- package/dist/clis/amazon/bestsellers.d.ts +0 -20
- package/dist/clis/amazon/bestsellers.js +6 -129
- package/dist/clis/amazon/bestsellers.test.js +12 -3
- package/dist/clis/amazon/movers-shakers.d.ts +1 -0
- package/dist/clis/amazon/movers-shakers.js +7 -0
- package/dist/clis/amazon/new-releases.d.ts +1 -0
- package/dist/clis/amazon/new-releases.js +7 -0
- package/dist/clis/amazon/rankings.d.ts +59 -0
- package/dist/clis/amazon/rankings.js +226 -0
- package/dist/clis/amazon/rankings.test.d.ts +1 -0
- package/dist/clis/amazon/rankings.test.js +41 -0
- package/dist/clis/amazon/shared.d.ts +11 -0
- package/dist/clis/amazon/shared.js +121 -11
- package/dist/clis/amazon/shared.test.js +11 -0
- package/dist/clis/bilibili/comments.js +2 -2
- package/dist/clis/bilibili/comments.test.js +3 -2
- package/dist/clis/bilibili/download.js +2 -1
- package/dist/clis/bilibili/subtitle.js +4 -3
- package/dist/clis/bilibili/subtitle.test.js +2 -1
- package/dist/clis/bilibili/utils.d.ts +5 -0
- package/dist/clis/bilibili/utils.js +30 -0
- package/dist/clis/bilibili/utils.test.d.ts +1 -0
- package/dist/clis/bilibili/utils.test.js +17 -0
- package/dist/clis/douban/marks.js +1 -1
- package/dist/clis/douban/subject.yaml +50 -19
- package/dist/clis/doubao/utils.js +32 -12
- package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
- package/dist/clis/douyin/_shared/transcode.test.js +0 -2
- package/dist/clis/douyin/draft.test.js +0 -2
- package/dist/clis/facebook/search.test.js +0 -2
- package/dist/clis/gemini/ask.js +9 -3
- package/dist/clis/gemini/ask.test.d.ts +1 -0
- package/dist/clis/gemini/ask.test.js +100 -0
- package/dist/clis/gemini/reply-state.test.d.ts +1 -0
- package/dist/clis/gemini/reply-state.test.js +641 -0
- package/dist/clis/gemini/utils.d.ts +44 -1
- package/dist/clis/gemini/utils.js +528 -61
- package/dist/clis/gemini/utils.test.js +149 -2
- package/dist/clis/hupu/detail.d.ts +1 -0
- package/dist/clis/hupu/detail.js +72 -0
- package/dist/clis/hupu/hot.yaml +43 -0
- package/dist/clis/hupu/like.d.ts +1 -0
- package/dist/clis/hupu/like.js +75 -0
- package/dist/clis/hupu/reply.d.ts +1 -0
- package/dist/clis/hupu/reply.js +71 -0
- package/dist/clis/hupu/search.d.ts +1 -0
- package/dist/clis/hupu/search.js +59 -0
- package/dist/clis/hupu/unlike.d.ts +1 -0
- package/dist/clis/hupu/unlike.js +75 -0
- package/dist/clis/hupu/utils.d.ts +20 -0
- package/dist/clis/hupu/utils.js +319 -0
- package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
- package/dist/clis/instagram/_shared/private-publish.js +1030 -0
- package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
- package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
- package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
- package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
- package/dist/clis/instagram/_shared/runtime-info.js +81 -0
- package/dist/clis/instagram/note.d.ts +1 -0
- package/dist/clis/instagram/note.js +222 -0
- package/dist/clis/instagram/note.test.d.ts +1 -0
- package/dist/clis/instagram/note.test.js +81 -0
- package/dist/clis/instagram/post.d.ts +4 -0
- package/dist/clis/instagram/post.js +1496 -0
- package/dist/clis/instagram/post.test.d.ts +1 -0
- package/dist/clis/instagram/post.test.js +1647 -0
- package/dist/clis/instagram/reel.d.ts +1 -0
- package/dist/clis/instagram/reel.js +826 -0
- package/dist/clis/instagram/reel.test.d.ts +1 -0
- package/dist/clis/instagram/reel.test.js +167 -0
- package/dist/clis/instagram/story.d.ts +1 -0
- package/dist/clis/instagram/story.js +115 -0
- package/dist/clis/instagram/story.test.d.ts +1 -0
- package/dist/clis/instagram/story.test.js +167 -0
- package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
- package/dist/clis/sinafinance/stock-rank.js +65 -0
- package/dist/clis/substack/utils.test.js +0 -2
- package/dist/clis/twitter/post.js +72 -45
- package/dist/clis/twitter/post.test.d.ts +1 -0
- package/dist/clis/twitter/post.test.js +116 -0
- package/dist/clis/twitter/reply.d.ts +12 -0
- package/dist/clis/twitter/reply.js +257 -35
- package/dist/clis/twitter/reply.test.d.ts +1 -0
- package/dist/clis/twitter/reply.test.js +151 -0
- package/dist/clis/twitter/search.js +67 -5
- package/dist/clis/twitter/search.test.js +83 -5
- package/dist/clis/xianyu/chat.d.ts +7 -0
- package/dist/clis/xianyu/chat.js +146 -0
- package/dist/clis/xianyu/chat.test.d.ts +1 -0
- package/dist/clis/xianyu/chat.test.js +15 -0
- package/dist/clis/xianyu/item.d.ts +7 -0
- package/dist/clis/xianyu/item.js +152 -0
- package/dist/clis/xianyu/item.test.d.ts +1 -0
- package/dist/clis/xianyu/item.test.js +56 -0
- package/dist/clis/xianyu/search.d.ts +10 -0
- package/dist/clis/xianyu/search.js +134 -0
- package/dist/clis/xianyu/search.test.d.ts +1 -0
- package/dist/clis/xianyu/search.test.js +17 -0
- package/dist/clis/xianyu/utils.d.ts +1 -0
- package/dist/clis/xianyu/utils.js +8 -0
- package/dist/clis/xiaoe/catalog.yaml +129 -0
- package/dist/clis/xiaoe/content.yaml +43 -0
- package/dist/clis/xiaoe/courses.yaml +73 -0
- package/dist/clis/xiaoe/detail.yaml +39 -0
- package/dist/clis/xiaoe/play-url.yaml +124 -0
- package/dist/clis/xiaohongshu/comments.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
- package/dist/clis/xiaohongshu/download.test.js +0 -2
- package/dist/clis/xiaohongshu/note.test.js +0 -2
- package/dist/clis/xiaohongshu/publish.test.js +0 -2
- package/dist/clis/xiaohongshu/search.js +29 -20
- package/dist/clis/xiaohongshu/search.test.js +56 -48
- package/dist/clis/yuanbao/ask.d.ts +21 -0
- package/dist/clis/yuanbao/ask.js +427 -0
- package/dist/clis/yuanbao/ask.test.d.ts +1 -0
- package/dist/clis/yuanbao/ask.test.js +124 -0
- package/dist/clis/yuanbao/new.d.ts +1 -0
- package/dist/clis/yuanbao/new.js +70 -0
- package/dist/clis/yuanbao/new.test.d.ts +1 -0
- package/dist/clis/yuanbao/new.test.js +30 -0
- package/dist/clis/yuanbao/shared.d.ts +13 -0
- package/dist/clis/yuanbao/shared.js +49 -0
- package/dist/clis/zhihu/question.js +30 -19
- package/dist/clis/zhihu/question.test.js +34 -16
- package/dist/commanderAdapter.js +8 -4
- package/dist/commanderAdapter.test.js +42 -0
- package/dist/completion.js +3 -1
- package/dist/completion.test.d.ts +1 -0
- package/dist/completion.test.js +23 -0
- package/dist/doctor.js +1 -1
- package/dist/electron-apps.d.ts +2 -0
- package/dist/electron-apps.js +7 -1
- package/dist/errors.js +1 -1
- package/dist/execution.js +25 -35
- package/dist/explore.js +1 -1
- package/dist/launcher.d.ts +4 -0
- package/dist/launcher.js +64 -8
- package/dist/launcher.test.js +88 -7
- package/dist/output.d.ts +2 -0
- package/dist/output.js +10 -1
- package/dist/output.test.d.ts +0 -3
- package/dist/output.test.js +59 -92
- package/dist/pipeline/executor.test.js +0 -2
- package/dist/pipeline/steps/download.test.js +0 -2
- package/dist/registry.d.ts +2 -0
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +1 -0
- package/dist/types.d.ts +9 -2
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/1688.md +52 -0
- package/docs/adapters/browser/36kr.md +2 -1
- package/docs/adapters/browser/doubao.md +5 -1
- package/docs/adapters/browser/hupu.md +53 -0
- package/docs/adapters/browser/sinafinance.md +32 -2
- package/docs/adapters/browser/weibo.md +6 -1
- package/docs/adapters/browser/wikipedia.md +2 -0
- package/docs/adapters/browser/xianyu.md +42 -0
- package/docs/adapters/browser/xiaoe.md +44 -0
- package/docs/adapters/browser/yuanbao.md +64 -0
- package/docs/adapters/index.md +14 -5
- package/docs/comparison.md +1 -1
- package/docs/developer/ai-workflow.md +2 -2
- package/docs/developer/contributing.md +1 -1
- package/docs/developer/testing.md +2 -0
- package/docs/guide/plugins.md +1 -0
- package/docs/guide/troubleshooting.md +11 -0
- package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
- package/docs/zh/guide/plugins.md +1 -0
- package/extension/dist/background.js +1127 -0
- package/extension/src/background.test.ts +39 -0
- package/extension/src/background.ts +223 -34
- package/extension/src/cdp.ts +194 -4
- package/extension/src/protocol.ts +22 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +1 -1
- package/skills/opencli-explorer/SKILL.md +1 -1
- package/skills/opencli-oneshot/SKILL.md +2 -2
- package/skills/opencli-operate/SKILL.md +120 -27
- package/skills/opencli-usage/SKILL.md +31 -20
- package/skills/opencli-usage/browser.md +114 -16
- package/skills/opencli-usage/public-api.md +32 -3
- package/skills/smart-search/SKILL.md +156 -0
- package/skills/smart-search/references/sources-ai.md +74 -0
- package/skills/smart-search/references/sources-info.md +43 -0
- package/skills/smart-search/references/sources-media.md +50 -0
- package/skills/smart-search/references/sources-other.md +42 -0
- package/skills/smart-search/references/sources-shopping.md +31 -0
- package/skills/smart-search/references/sources-social.md +51 -0
- package/skills/smart-search/references/sources-tech.md +42 -0
- package/skills/smart-search/references/sources-travel.md +20 -0
- package/src/browser/base-page.ts +41 -6
- package/src/browser/bridge.ts +11 -8
- package/src/browser/cdp.ts +1 -8
- package/src/browser/daemon-client.ts +11 -1
- package/src/browser/dom-helpers.ts +43 -31
- package/src/browser/dom-snapshot.ts +23 -1
- package/src/browser/page.ts +115 -31
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.ts +2 -0
- package/src/cli.test.ts +133 -0
- package/src/cli.ts +73 -11
- package/src/clis/1688/item.test.ts +69 -0
- package/src/clis/1688/item.ts +282 -0
- package/src/clis/1688/search.test.ts +81 -0
- package/src/clis/1688/search.ts +402 -0
- package/src/clis/1688/shared.test.ts +75 -0
- package/src/clis/1688/shared.ts +623 -0
- package/src/clis/1688/store.test.ts +69 -0
- package/src/clis/1688/store.ts +300 -0
- package/src/clis/amazon/bestsellers.test.ts +12 -3
- package/src/clis/amazon/bestsellers.ts +6 -178
- package/src/clis/amazon/movers-shakers.ts +8 -0
- package/src/clis/amazon/new-releases.ts +8 -0
- package/src/clis/amazon/rankings.test.ts +47 -0
- package/src/clis/amazon/rankings.ts +312 -0
- package/src/clis/amazon/shared.test.ts +16 -0
- package/src/clis/amazon/shared.ts +134 -12
- package/src/clis/bilibili/comments.test.ts +4 -3
- package/src/clis/bilibili/comments.ts +2 -2
- package/src/clis/bilibili/download.ts +2 -1
- package/src/clis/bilibili/subtitle.test.ts +2 -1
- package/src/clis/bilibili/subtitle.ts +4 -3
- package/src/clis/bilibili/utils.test.ts +21 -0
- package/src/clis/bilibili/utils.ts +27 -0
- package/src/clis/douban/marks.ts +1 -1
- package/src/clis/douban/subject.yaml +50 -19
- package/src/clis/doubao/utils.ts +32 -12
- package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
- package/src/clis/douyin/_shared/transcode.test.ts +0 -2
- package/src/clis/douyin/draft.test.ts +0 -2
- package/src/clis/facebook/search.test.ts +0 -2
- package/src/clis/gemini/ask.test.ts +116 -0
- package/src/clis/gemini/ask.ts +10 -3
- package/src/clis/gemini/reply-state.test.ts +708 -0
- package/src/clis/gemini/utils.test.ts +184 -2
- package/src/clis/gemini/utils.ts +588 -60
- package/src/clis/hupu/detail.ts +126 -0
- package/src/clis/hupu/hot.yaml +43 -0
- package/src/clis/hupu/like.ts +76 -0
- package/src/clis/hupu/reply.ts +76 -0
- package/src/clis/hupu/search.ts +95 -0
- package/src/clis/hupu/unlike.ts +76 -0
- package/src/clis/hupu/utils.ts +381 -0
- package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
- package/src/clis/instagram/_shared/private-publish.ts +1303 -0
- package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
- package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
- package/src/clis/instagram/_shared/runtime-info.ts +91 -0
- package/src/clis/instagram/note.test.ts +96 -0
- package/src/clis/instagram/note.ts +254 -0
- package/src/clis/instagram/post.test.ts +1716 -0
- package/src/clis/instagram/post.ts +1620 -0
- package/src/clis/instagram/reel.test.ts +191 -0
- package/src/clis/instagram/reel.ts +886 -0
- package/src/clis/instagram/story.test.ts +191 -0
- package/src/clis/instagram/story.ts +151 -0
- package/src/clis/sinafinance/stock-rank.ts +68 -0
- package/src/clis/substack/utils.test.ts +0 -2
- package/src/clis/twitter/post.test.ts +157 -0
- package/src/clis/twitter/post.ts +82 -48
- package/src/clis/twitter/reply.test.ts +177 -0
- package/src/clis/twitter/reply.ts +285 -39
- package/src/clis/twitter/search.test.ts +88 -5
- package/src/clis/twitter/search.ts +68 -5
- package/src/clis/xianyu/chat.test.ts +20 -0
- package/src/clis/xianyu/chat.ts +175 -0
- package/src/clis/xianyu/item.test.ts +67 -0
- package/src/clis/xianyu/item.ts +172 -0
- package/src/clis/xianyu/search.test.ts +22 -0
- package/src/clis/xianyu/search.ts +151 -0
- package/src/clis/xianyu/utils.ts +9 -0
- package/src/clis/xiaoe/catalog.yaml +129 -0
- package/src/clis/xiaoe/content.yaml +43 -0
- package/src/clis/xiaoe/courses.yaml +73 -0
- package/src/clis/xiaoe/detail.yaml +39 -0
- package/src/clis/xiaoe/play-url.yaml +124 -0
- package/src/clis/xiaohongshu/comments.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
- package/src/clis/xiaohongshu/download.test.ts +0 -2
- package/src/clis/xiaohongshu/note.test.ts +0 -2
- package/src/clis/xiaohongshu/publish.test.ts +0 -2
- package/src/clis/xiaohongshu/search.test.ts +59 -48
- package/src/clis/xiaohongshu/search.ts +31 -21
- package/src/clis/yuanbao/ask.test.ts +156 -0
- package/src/clis/yuanbao/ask.ts +522 -0
- package/src/clis/yuanbao/new.test.ts +36 -0
- package/src/clis/yuanbao/new.ts +81 -0
- package/src/clis/yuanbao/shared.ts +57 -0
- package/src/clis/zhihu/question.test.ts +42 -17
- package/src/clis/zhihu/question.ts +31 -26
- package/src/commanderAdapter.test.ts +51 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/completion.test.ts +30 -0
- package/src/completion.ts +3 -1
- package/src/doctor.ts +1 -1
- package/src/electron-apps.ts +9 -1
- package/src/errors.ts +1 -1
- package/src/execution.ts +26 -30
- package/src/explore.ts +1 -1
- package/src/launcher.test.ts +121 -7
- package/src/launcher.ts +87 -9
- package/src/output.test.ts +50 -90
- package/src/output.ts +10 -1
- package/src/pipeline/executor.test.ts +0 -2
- package/src/pipeline/steps/download.test.ts +0 -2
- package/src/registry.ts +2 -0
- package/src/serialization.ts +2 -0
- package/src/types.ts +9 -2
- package/tests/e2e/browser-auth.test.ts +9 -0
- package/CLI-EXPLORER.md +0 -724
- package/CLI-ONESHOT.md +0 -216
- package/SKILL.md +0 -59
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { CommandExecutionError, EmptyResultError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import { FACTORY_BADGE_PATTERNS, SERVICE_BADGE_PATTERNS, assertAuthenticatedState, buildDetailUrl, buildProvenance, canonicalizeSellerUrl, canonicalizeStoreUrl, cleanMultilineText, cleanText, extractAddress, extractBadges, extractMemberId, extractMetric, extractOfferId, extractShopId, extractYearsOnPlatform, gotoAndReadState, guessTopCategories, resolveStoreUrl, uniqueNonEmpty, } from './shared.js';
|
|
4
|
+
function normalizeStorePayload(input) {
|
|
5
|
+
const storePayload = input.storePayload;
|
|
6
|
+
const contactPayload = input.contactPayload;
|
|
7
|
+
const seed = input.seed;
|
|
8
|
+
const contactText = cleanMultilineText(contactPayload?.bodyText);
|
|
9
|
+
const storeText = cleanMultilineText(storePayload?.bodyText);
|
|
10
|
+
const seedText = cleanMultilineText(seed?.bodyText);
|
|
11
|
+
const combinedText = [contactText, storeText, seedText].filter(Boolean).join('\n');
|
|
12
|
+
const sellerUrlRaw = cleanText(seed?.seller?.winportUrl
|
|
13
|
+
?? seed?.seller?.sellerWinportUrlMap?.defaultUrl
|
|
14
|
+
?? storePayload?.href
|
|
15
|
+
?? input.resolvedUrl);
|
|
16
|
+
const storeUrl = safeCanonicalStoreUrl(sellerUrlRaw || input.resolvedUrl) ?? input.resolvedUrl;
|
|
17
|
+
const sellerUrl = canonicalizeSellerUrl(sellerUrlRaw) ?? storeUrl;
|
|
18
|
+
const companyUrl = pickCompanyUrl(contactPayload?.href, storeUrl);
|
|
19
|
+
const memberId = cleanText(seed?.seller?.memberId)
|
|
20
|
+
|| input.explicitMemberId
|
|
21
|
+
|| extractMemberId(input.resolvedUrl)
|
|
22
|
+
|| extractMemberId(storePayload?.href ?? '')
|
|
23
|
+
|| null;
|
|
24
|
+
const shopId = extractShopId(sellerUrl) ?? extractShopId(storeUrl);
|
|
25
|
+
const companyName = cleanText(seed?.seller?.companyName)
|
|
26
|
+
|| firstNamedLine(contactText)
|
|
27
|
+
|| firstNamedLine(storeText)
|
|
28
|
+
|| null;
|
|
29
|
+
const serviceBadges = uniqueNonEmpty([
|
|
30
|
+
...extractBadges(combinedText, SERVICE_BADGE_PATTERNS),
|
|
31
|
+
...((seed?.services ?? []).map((service) => cleanText(service.serviceName))),
|
|
32
|
+
]);
|
|
33
|
+
const factoryBadges = extractBadges(combinedText, FACTORY_BADGE_PATTERNS);
|
|
34
|
+
return {
|
|
35
|
+
member_id: memberId,
|
|
36
|
+
shop_id: shopId,
|
|
37
|
+
store_name: companyName,
|
|
38
|
+
store_url: storeUrl,
|
|
39
|
+
company_name: companyName,
|
|
40
|
+
company_url: companyUrl,
|
|
41
|
+
business_model_text: firstMetric(combinedText, ['经营模式', '生产加工', '主营产品']),
|
|
42
|
+
years_on_platform_text: extractYearsOnPlatform(combinedText),
|
|
43
|
+
location: extractAddress(contactText) ?? extractAddress(storeText),
|
|
44
|
+
staff_size_text: firstMetric(combinedText, ['员工人数', '员工总数']),
|
|
45
|
+
factory_badges: factoryBadges,
|
|
46
|
+
service_badges: serviceBadges,
|
|
47
|
+
response_rate_text: firstMetric(combinedText, ['响应率', '回复率', '响应速度']),
|
|
48
|
+
return_rate_text: extractReturnRate(combinedText),
|
|
49
|
+
top_categories: guessTopCategories(combinedText),
|
|
50
|
+
phone_text: extractMetric(contactText, '电话'),
|
|
51
|
+
mobile_text: extractMetric(contactText, '手机'),
|
|
52
|
+
...buildProvenance(cleanText(contactPayload?.href) || cleanText(storePayload?.href) || input.resolvedUrl),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function safeCanonicalStoreUrl(url) {
|
|
56
|
+
try {
|
|
57
|
+
return canonicalizeStoreUrl(url);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function pickCompanyUrl(contactHref, storeUrl) {
|
|
64
|
+
const fromPage = cleanText(contactHref);
|
|
65
|
+
if (fromPage) {
|
|
66
|
+
const normalized = buildContactUrl(fromPage);
|
|
67
|
+
if (normalized)
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
return buildContactUrl(storeUrl);
|
|
71
|
+
}
|
|
72
|
+
function buildContactUrl(storeUrl) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = new URL(storeUrl);
|
|
75
|
+
if (!parsed.hostname.endsWith('.1688.com'))
|
|
76
|
+
return null;
|
|
77
|
+
return `${parsed.protocol}//${parsed.hostname}/page/contactinfo.html`;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function firstNamedLine(text) {
|
|
84
|
+
return text
|
|
85
|
+
.split('\n')
|
|
86
|
+
.map((line) => cleanText(line))
|
|
87
|
+
.find((line) => line.includes('有限公司') || line.includes('商行') || line.includes('工厂'))
|
|
88
|
+
?? null;
|
|
89
|
+
}
|
|
90
|
+
function firstMetric(text, labels) {
|
|
91
|
+
for (const label of labels) {
|
|
92
|
+
const value = extractMetric(text, label);
|
|
93
|
+
if (value)
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function extractReturnRate(text) {
|
|
99
|
+
const inline = text.match(/回头率\s*([0-9.]+%)/);
|
|
100
|
+
if (inline)
|
|
101
|
+
return cleanText(inline[0]);
|
|
102
|
+
const multiline = text.match(/回头率\s*\n\s*([0-9.]+%)/);
|
|
103
|
+
if (!multiline)
|
|
104
|
+
return null;
|
|
105
|
+
return `回头率${cleanText(multiline[1])}`;
|
|
106
|
+
}
|
|
107
|
+
function firstOfferId(links) {
|
|
108
|
+
for (const link of links) {
|
|
109
|
+
const offerId = extractOfferId(link);
|
|
110
|
+
if (offerId)
|
|
111
|
+
return offerId;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function firstContactUrl(links) {
|
|
116
|
+
for (const link of links) {
|
|
117
|
+
const url = buildContactUrl(link);
|
|
118
|
+
if (url)
|
|
119
|
+
return url;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
async function readStorePayload(page, url, action) {
|
|
124
|
+
const state = await gotoAndReadState(page, url, 2500, action);
|
|
125
|
+
assertAuthenticatedState(state, action);
|
|
126
|
+
return await page.evaluate(`
|
|
127
|
+
(() => ({
|
|
128
|
+
href: window.location.href,
|
|
129
|
+
title: document.title || '',
|
|
130
|
+
bodyText: document.body ? document.body.innerText || '' : '',
|
|
131
|
+
offerLinks: Array.from(document.querySelectorAll('a[href*="detail.1688.com/offer/"], a[href*="offerId="]'))
|
|
132
|
+
.map((anchor) => anchor.href)
|
|
133
|
+
.filter(Boolean),
|
|
134
|
+
contactLinks: Array.from(document.querySelectorAll('a[href*="contactinfo"]'))
|
|
135
|
+
.map((anchor) => anchor.href)
|
|
136
|
+
.filter(Boolean),
|
|
137
|
+
}))()
|
|
138
|
+
`);
|
|
139
|
+
}
|
|
140
|
+
async function readItemSeed(page, offerId) {
|
|
141
|
+
const itemUrl = buildDetailUrl(offerId);
|
|
142
|
+
const state = await gotoAndReadState(page, itemUrl, 2500, 'store seed item');
|
|
143
|
+
assertAuthenticatedState(state, 'store seed item');
|
|
144
|
+
const seed = await page.evaluate(`
|
|
145
|
+
(() => {
|
|
146
|
+
const model = window.context?.result?.global?.globalData?.model ?? null;
|
|
147
|
+
const toJson = (value) => JSON.parse(JSON.stringify(value ?? null));
|
|
148
|
+
return {
|
|
149
|
+
href: window.location.href,
|
|
150
|
+
bodyText: document.body ? document.body.innerText || '' : '',
|
|
151
|
+
seller: toJson(model?.sellerModel),
|
|
152
|
+
services: toJson(model?.shippingServices?.fields?.buyerProtectionModel ?? []),
|
|
153
|
+
};
|
|
154
|
+
})()
|
|
155
|
+
`);
|
|
156
|
+
const hasSellerContext = !!cleanText(seed?.seller?.memberId) || !!cleanText(seed?.seller?.winportUrl);
|
|
157
|
+
if (!hasSellerContext) {
|
|
158
|
+
throw new CommandExecutionError('1688 store seed item did not expose seller context', '当前 tab 非商品详情上下文,请切到 detail.1688.com 商品页并重试');
|
|
159
|
+
}
|
|
160
|
+
return seed;
|
|
161
|
+
}
|
|
162
|
+
function hasAnyEvidence(storePayload, contactPayload, seed) {
|
|
163
|
+
return !!cleanText(storePayload?.bodyText)
|
|
164
|
+
|| !!cleanText(contactPayload?.bodyText)
|
|
165
|
+
|| !!cleanText(seed?.bodyText);
|
|
166
|
+
}
|
|
167
|
+
cli({
|
|
168
|
+
site: '1688',
|
|
169
|
+
name: 'store',
|
|
170
|
+
description: '1688 店铺/供应商公开信息(联系方式、主营、入驻年限、公开服务信号)',
|
|
171
|
+
domain: 'www.1688.com',
|
|
172
|
+
strategy: Strategy.COOKIE,
|
|
173
|
+
navigateBefore: false,
|
|
174
|
+
args: [
|
|
175
|
+
{
|
|
176
|
+
name: 'input',
|
|
177
|
+
required: true,
|
|
178
|
+
positional: true,
|
|
179
|
+
help: '1688 店铺 URL 或 member ID(如 b2b-22154705262941f196)',
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
columns: ['store_name', 'years_on_platform_text', 'location', 'return_rate_text'],
|
|
183
|
+
func: async (page, kwargs) => {
|
|
184
|
+
const rawInput = String(kwargs.input ?? '');
|
|
185
|
+
const resolvedUrl = resolveStoreUrl(rawInput);
|
|
186
|
+
const explicitMemberId = extractMemberId(rawInput);
|
|
187
|
+
const storePayload = await readStorePayload(page, resolvedUrl, 'store');
|
|
188
|
+
const contactUrl = firstContactUrl(storePayload.contactLinks ?? []) || buildContactUrl(storePayload.href || resolvedUrl);
|
|
189
|
+
const contactPayload = contactUrl ? await readStorePayload(page, contactUrl, 'store contact') : null;
|
|
190
|
+
const offerId = extractOfferId(rawInput)
|
|
191
|
+
|| firstOfferId(storePayload.offerLinks ?? [])
|
|
192
|
+
|| firstOfferId(contactPayload?.offerLinks ?? []);
|
|
193
|
+
let seed = null;
|
|
194
|
+
if (offerId) {
|
|
195
|
+
try {
|
|
196
|
+
seed = await readItemSeed(page, offerId);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (!(error instanceof CommandExecutionError))
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!hasAnyEvidence(storePayload, contactPayload, seed)) {
|
|
204
|
+
throw new EmptyResultError('1688 store', 'Store page is reachable but no visible fields were extracted. Open the store page in Chrome and retry.');
|
|
205
|
+
}
|
|
206
|
+
return [
|
|
207
|
+
normalizeStorePayload({
|
|
208
|
+
resolvedUrl,
|
|
209
|
+
storePayload,
|
|
210
|
+
contactPayload,
|
|
211
|
+
seed,
|
|
212
|
+
explicitMemberId,
|
|
213
|
+
}),
|
|
214
|
+
];
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
export const __test__ = {
|
|
218
|
+
normalizeStorePayload,
|
|
219
|
+
safeCanonicalStoreUrl,
|
|
220
|
+
buildContactUrl,
|
|
221
|
+
firstNamedLine,
|
|
222
|
+
firstMetric,
|
|
223
|
+
extractReturnRate,
|
|
224
|
+
firstOfferId,
|
|
225
|
+
firstContactUrl,
|
|
226
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __test__ } from './store.js';
|
|
3
|
+
describe('1688 store normalization', () => {
|
|
4
|
+
it('merges store contact text with seller seed data', () => {
|
|
5
|
+
const result = __test__.normalizeStorePayload({
|
|
6
|
+
resolvedUrl: 'https://yinuoweierfushi.1688.com/?offerId=887904326744',
|
|
7
|
+
explicitMemberId: null,
|
|
8
|
+
storePayload: {
|
|
9
|
+
href: 'https://yinuoweierfushi.1688.com/page/index.html',
|
|
10
|
+
bodyText: `
|
|
11
|
+
青岛沁澜衣品服装有限公司
|
|
12
|
+
联系方式
|
|
13
|
+
地址:山东省青岛市即墨区环秀街道办事处湘江二路97号甲
|
|
14
|
+
`,
|
|
15
|
+
offerLinks: ['https://detail.1688.com/offer/887904326744.html'],
|
|
16
|
+
},
|
|
17
|
+
contactPayload: {
|
|
18
|
+
href: 'https://yinuoweierfushi.1688.com/page/contactinfo.html',
|
|
19
|
+
bodyText: `
|
|
20
|
+
青岛沁澜衣品服装有限公司
|
|
21
|
+
电话:86 0532 86655366
|
|
22
|
+
手机:15963238678
|
|
23
|
+
地址:山东省青岛市即墨区环秀街道办事处湘江二路97号甲
|
|
24
|
+
`,
|
|
25
|
+
},
|
|
26
|
+
seed: {
|
|
27
|
+
bodyText: `
|
|
28
|
+
入驻13年
|
|
29
|
+
主营:大码女装
|
|
30
|
+
店铺回头率
|
|
31
|
+
87%
|
|
32
|
+
延期必赔
|
|
33
|
+
品质保障
|
|
34
|
+
`,
|
|
35
|
+
seller: {
|
|
36
|
+
companyName: '青岛沁澜衣品服装有限公司',
|
|
37
|
+
memberId: 'b2b-1641351767',
|
|
38
|
+
winportUrl: 'https://yinuoweierfushi.1688.com/page/index.html?spm=abc',
|
|
39
|
+
},
|
|
40
|
+
services: [{ serviceName: '延期必赔' }, { serviceName: '品质保障' }],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(result.member_id).toBe('b2b-1641351767');
|
|
44
|
+
expect(result.store_url).toBe('https://yinuoweierfushi.1688.com');
|
|
45
|
+
expect(result.company_url).toBe('https://yinuoweierfushi.1688.com/page/contactinfo.html');
|
|
46
|
+
expect(result.years_on_platform_text).toBe('入驻13年');
|
|
47
|
+
expect(result.location).toBe('山东省青岛市即墨区环秀街道办事处湘江二路97号甲');
|
|
48
|
+
expect(result.return_rate_text).toContain('87%');
|
|
49
|
+
expect(result.top_categories).toEqual(['大码女装']);
|
|
50
|
+
expect(result.service_badges).toEqual(['延期必赔', '品质保障']);
|
|
51
|
+
});
|
|
52
|
+
it('builds contact urls and extracts offer ids', () => {
|
|
53
|
+
expect(__test__.safeCanonicalStoreUrl('https://yinuoweierfushi.1688.com/page/index.html?spm=foo')).toBe('https://yinuoweierfushi.1688.com');
|
|
54
|
+
expect(__test__.buildContactUrl('https://yinuoweierfushi.1688.com')).toBe('https://yinuoweierfushi.1688.com/page/contactinfo.html');
|
|
55
|
+
expect(__test__.firstOfferId([
|
|
56
|
+
'https://detail.1688.com/offer/887904326744.html',
|
|
57
|
+
])).toBe('887904326744');
|
|
58
|
+
expect(__test__.firstContactUrl([
|
|
59
|
+
'https://yinuoweierfushi.1688.com/page/contactinfo.html?spm=1',
|
|
60
|
+
])).toBe('https://yinuoweierfushi.1688.com/page/contactinfo.html');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
interface BestsellersPagePayload {
|
|
2
|
-
href?: string;
|
|
3
|
-
title?: string;
|
|
4
|
-
list_title?: string;
|
|
5
|
-
cards?: Array<{
|
|
6
|
-
rank_text?: string | null;
|
|
7
|
-
asin?: string | null;
|
|
8
|
-
title?: string | null;
|
|
9
|
-
href?: string | null;
|
|
10
|
-
price_text?: string | null;
|
|
11
|
-
rating_text?: string | null;
|
|
12
|
-
review_count_text?: string | null;
|
|
13
|
-
card_text?: string | null;
|
|
14
|
-
}>;
|
|
15
|
-
page_links?: string[];
|
|
16
|
-
}
|
|
17
|
-
declare function normalizeBestsellerCandidate(candidate: NonNullable<BestsellersPagePayload['cards']>[number], rank: number, listTitle: string | null, sourceUrl: string): Record<string, unknown>;
|
|
18
|
-
export declare const __test__: {
|
|
19
|
-
normalizeBestsellerCandidate: typeof normalizeBestsellerCandidate;
|
|
20
|
-
};
|
|
21
1
|
export {};
|
|
@@ -1,130 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const asin = extractAsin(candidate.asin ?? '') ?? extractAsin(productUrl ?? '') ?? null;
|
|
7
|
-
const title = cleanText(candidate.title) || firstMeaningfulLine(candidate.card_text);
|
|
8
|
-
const price = parsePriceText(cleanText(candidate.price_text) || candidate.card_text);
|
|
9
|
-
const ratingText = cleanText(candidate.rating_text) || null;
|
|
10
|
-
const reviewCountText = cleanText(candidate.review_count_text)
|
|
11
|
-
|| extractReviewCountFromCardText(candidate.card_text)
|
|
12
|
-
|| null;
|
|
13
|
-
const provenance = buildProvenance(sourceUrl);
|
|
14
|
-
return {
|
|
15
|
-
rank,
|
|
16
|
-
asin,
|
|
17
|
-
title: title || null,
|
|
18
|
-
product_url: productUrl,
|
|
19
|
-
list_title: listTitle,
|
|
20
|
-
...provenance,
|
|
21
|
-
price_text: price.price_text,
|
|
22
|
-
price_value: price.price_value,
|
|
23
|
-
currency: price.currency,
|
|
24
|
-
rating_text: ratingText,
|
|
25
|
-
rating_value: parseRatingValue(ratingText),
|
|
26
|
-
review_count_text: reviewCountText,
|
|
27
|
-
review_count: parseReviewCount(reviewCountText),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
async function readBestsellersPage(page, url) {
|
|
31
|
-
const state = await gotoAndReadState(page, url, 2500, 'bestsellers');
|
|
32
|
-
assertUsableState(state, 'bestsellers');
|
|
33
|
-
return await page.evaluate(`
|
|
34
|
-
(() => ({
|
|
35
|
-
href: window.location.href,
|
|
36
|
-
title: document.title || '',
|
|
37
|
-
list_title:
|
|
38
|
-
document.querySelector('#zg_banner_text')?.textContent
|
|
39
|
-
|| document.querySelector('h1')?.textContent
|
|
40
|
-
|| '',
|
|
41
|
-
cards: Array.from(document.querySelectorAll('.p13n-sc-uncoverable-faceout'))
|
|
42
|
-
.map((card) => ({
|
|
43
|
-
rank_text:
|
|
44
|
-
card.querySelector('.zg-bdg-text')?.textContent
|
|
45
|
-
|| card.querySelector('[class*="rank"]')?.textContent
|
|
46
|
-
|| '',
|
|
47
|
-
asin: card.id || '',
|
|
48
|
-
title:
|
|
49
|
-
card.querySelector('[class*="line-clamp"]')?.textContent
|
|
50
|
-
|| card.querySelector('img')?.getAttribute('alt')
|
|
51
|
-
|| '',
|
|
52
|
-
href: card.querySelector('a[href*="/dp/"]')?.href || '',
|
|
53
|
-
price_text: card.querySelector('.a-price .a-offscreen')?.textContent || '',
|
|
54
|
-
rating_text: card.querySelector('[aria-label*="out of 5 stars"]')?.getAttribute('aria-label') || '',
|
|
55
|
-
review_count_text:
|
|
56
|
-
card.querySelector('a[href*="#customerReviews"]')?.textContent
|
|
57
|
-
|| card.querySelector('.a-size-small')?.textContent
|
|
58
|
-
|| '',
|
|
59
|
-
card_text: card.innerText || '',
|
|
60
|
-
})),
|
|
61
|
-
page_links: Array.from(document.querySelectorAll('li.a-normal a, li.a-selected a'))
|
|
62
|
-
.map((anchor) => anchor.href || '')
|
|
63
|
-
.filter((href) => /\\/zgbs\\//.test(href) && /(?:[?&]pg=|ref=zg_bs_pg_)/.test(href)),
|
|
64
|
-
}))()
|
|
65
|
-
`);
|
|
66
|
-
}
|
|
67
|
-
cli({
|
|
68
|
-
site: 'amazon',
|
|
69
|
-
name: 'bestsellers',
|
|
1
|
+
import { cli } from '../../registry.js';
|
|
2
|
+
import { createRankingCliOptions } from './rankings.js';
|
|
3
|
+
cli(createRankingCliOptions({
|
|
4
|
+
commandName: 'bestsellers',
|
|
5
|
+
listType: 'bestsellers',
|
|
70
6
|
description: 'Amazon Best Sellers pages for category candidate discovery',
|
|
71
|
-
|
|
72
|
-
strategy: Strategy.COOKIE,
|
|
73
|
-
navigateBefore: false,
|
|
74
|
-
args: [
|
|
75
|
-
{
|
|
76
|
-
name: 'input',
|
|
77
|
-
positional: true,
|
|
78
|
-
help: 'Best sellers URL or /zgbs path. Omit to use the root Best Sellers page.',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: 'limit',
|
|
82
|
-
type: 'int',
|
|
83
|
-
default: 100,
|
|
84
|
-
help: 'Maximum number of ranked items to return (default 100)',
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
columns: ['rank', 'asin', 'title', 'price_text', 'rating_value', 'review_count'],
|
|
88
|
-
func: async (page, kwargs) => {
|
|
89
|
-
const limit = Math.max(1, Number(kwargs.limit) || 100);
|
|
90
|
-
const initialUrl = resolveBestsellersUrl(typeof kwargs.input === 'string' ? kwargs.input : undefined);
|
|
91
|
-
const queue = [initialUrl];
|
|
92
|
-
const visited = new Set();
|
|
93
|
-
const seenAsins = new Set();
|
|
94
|
-
const results = [];
|
|
95
|
-
let listTitle = null;
|
|
96
|
-
while (queue.length > 0 && results.length < limit) {
|
|
97
|
-
const nextUrl = queue.shift();
|
|
98
|
-
if (visited.has(nextUrl))
|
|
99
|
-
continue;
|
|
100
|
-
visited.add(nextUrl);
|
|
101
|
-
const payload = await readBestsellersPage(page, nextUrl);
|
|
102
|
-
const sourceUrl = cleanText(payload.href) || nextUrl;
|
|
103
|
-
listTitle = cleanText(payload.list_title) || cleanText(payload.title) || listTitle;
|
|
104
|
-
const cards = payload.cards ?? [];
|
|
105
|
-
for (const card of cards) {
|
|
106
|
-
const normalized = normalizeBestsellerCandidate(card, results.length + 1, listTitle, sourceUrl);
|
|
107
|
-
const asin = cleanText(String(normalized.asin ?? ''));
|
|
108
|
-
if (!asin || seenAsins.has(asin))
|
|
109
|
-
continue;
|
|
110
|
-
seenAsins.add(asin);
|
|
111
|
-
results.push(normalized);
|
|
112
|
-
if (results.length >= limit)
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
const pageLinks = uniqueNonEmpty(payload.page_links ?? []);
|
|
116
|
-
for (const href of pageLinks) {
|
|
117
|
-
if (!visited.has(href) && !queue.includes(href)) {
|
|
118
|
-
queue.push(href);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (results.length === 0) {
|
|
123
|
-
throw new CommandExecutionError('amazon bestsellers did not expose any ranked items', 'Open the same best sellers page in Chrome, verify it is a real Amazon ranking page, and retry.');
|
|
124
|
-
}
|
|
125
|
-
return results.slice(0, limit);
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
export const __test__ = {
|
|
129
|
-
normalizeBestsellerCandidate,
|
|
130
|
-
};
|
|
7
|
+
}));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { __test__ } from './
|
|
2
|
+
import { __test__ } from './rankings.js';
|
|
3
3
|
describe('amazon bestsellers normalization', () => {
|
|
4
4
|
it('normalizes bestseller cards and infers review counts from card text', () => {
|
|
5
|
-
const result = __test__.
|
|
5
|
+
const result = __test__.normalizeRankingCandidate({
|
|
6
6
|
asin: 'B0DR31GC3D',
|
|
7
7
|
title: '',
|
|
8
8
|
href: 'https://www.amazon.com/NUTIKAS-Shelves-Desktop-Orgnizer-Shlef/dp/B0DR31GC3D/ref=zg_bs',
|
|
@@ -10,7 +10,16 @@ describe('amazon bestsellers normalization', () => {
|
|
|
10
10
|
rating_text: '4.3 out of 5 stars',
|
|
11
11
|
review_count_text: '',
|
|
12
12
|
card_text: 'Desk Shelves Desktop Organizer Shlef\n4.3 out of 5 stars\n435\n$25.92',
|
|
13
|
-
},
|
|
13
|
+
}, {
|
|
14
|
+
listType: 'bestsellers',
|
|
15
|
+
rankFallback: 2,
|
|
16
|
+
listTitle: 'Amazon Best Sellers: Best Desktop & Off-Surface Shelves',
|
|
17
|
+
sourceUrl: 'https://www.amazon.com/example',
|
|
18
|
+
categoryTitle: null,
|
|
19
|
+
categoryUrl: 'https://www.amazon.com/example',
|
|
20
|
+
categoryPath: [],
|
|
21
|
+
visibleCategoryLinks: [],
|
|
22
|
+
});
|
|
14
23
|
expect(result.rank).toBe(2);
|
|
15
24
|
expect(result.asin).toBe('B0DR31GC3D');
|
|
16
25
|
expect(result.title).toBe('Desk Shelves Desktop Organizer Shlef');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { cli } from '../../registry.js';
|
|
2
|
+
import { createRankingCliOptions } from './rankings.js';
|
|
3
|
+
cli(createRankingCliOptions({
|
|
4
|
+
commandName: 'movers-shakers',
|
|
5
|
+
listType: 'movers_shakers',
|
|
6
|
+
description: 'Amazon Movers & Shakers pages for short-term growth signals',
|
|
7
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { cli } from '../../registry.js';
|
|
2
|
+
import { createRankingCliOptions } from './rankings.js';
|
|
3
|
+
cli(createRankingCliOptions({
|
|
4
|
+
commandName: 'new-releases',
|
|
5
|
+
listType: 'new_releases',
|
|
6
|
+
description: 'Amazon New Releases pages for early momentum discovery',
|
|
7
|
+
}));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type CliOptions } from '../../registry.js';
|
|
2
|
+
import { type AmazonRankingListType } from './shared.js';
|
|
3
|
+
export interface RankingCardPayload {
|
|
4
|
+
rank_text?: string | null;
|
|
5
|
+
asin?: string | null;
|
|
6
|
+
title?: string | null;
|
|
7
|
+
href?: string | null;
|
|
8
|
+
price_text?: string | null;
|
|
9
|
+
rating_text?: string | null;
|
|
10
|
+
review_count_text?: string | null;
|
|
11
|
+
card_text?: string | null;
|
|
12
|
+
}
|
|
13
|
+
interface RankingPagePayload {
|
|
14
|
+
href?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
list_title?: string;
|
|
17
|
+
category_title?: string;
|
|
18
|
+
category_path?: string[];
|
|
19
|
+
cards?: RankingCardPayload[];
|
|
20
|
+
page_links?: string[];
|
|
21
|
+
visible_category_links?: Array<{
|
|
22
|
+
title?: string | null;
|
|
23
|
+
url?: string | null;
|
|
24
|
+
node_id?: string | null;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
interface RankingCommandDefinition {
|
|
28
|
+
commandName: string;
|
|
29
|
+
listType: AmazonRankingListType;
|
|
30
|
+
description: string;
|
|
31
|
+
}
|
|
32
|
+
interface RankingNormalizeContext {
|
|
33
|
+
listType: AmazonRankingListType;
|
|
34
|
+
rankFallback: number;
|
|
35
|
+
listTitle: string | null;
|
|
36
|
+
sourceUrl: string;
|
|
37
|
+
categoryTitle: string | null;
|
|
38
|
+
categoryUrl: string | null;
|
|
39
|
+
categoryPath: string[];
|
|
40
|
+
visibleCategoryLinks: Array<{
|
|
41
|
+
title: string;
|
|
42
|
+
url: string;
|
|
43
|
+
node_id: string | null;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
declare function parseRank(rawRank: string | null | undefined, fallback: number): number;
|
|
47
|
+
declare function normalizeVisibleCategoryLinks(links: RankingPagePayload['visible_category_links']): Array<{
|
|
48
|
+
title: string;
|
|
49
|
+
url: string;
|
|
50
|
+
node_id: string | null;
|
|
51
|
+
}>;
|
|
52
|
+
export declare function normalizeRankingCandidate(candidate: RankingCardPayload, context: RankingNormalizeContext): Record<string, unknown>;
|
|
53
|
+
export declare function createRankingCliOptions(definition: RankingCommandDefinition): CliOptions;
|
|
54
|
+
export declare const __test__: {
|
|
55
|
+
parseRank: typeof parseRank;
|
|
56
|
+
normalizeVisibleCategoryLinks: typeof normalizeVisibleCategoryLinks;
|
|
57
|
+
normalizeRankingCandidate: typeof normalizeRankingCandidate;
|
|
58
|
+
};
|
|
59
|
+
export {};
|