@jackwener/opencli 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/pull_request_template.md +3 -1
- package/.github/workflows/build-extension.yml +7 -1
- package/.github/workflows/ci.yml +29 -3
- package/.github/workflows/docs.yml +1 -1
- package/.github/workflows/e2e-headed.yml +20 -0
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/security.yml +0 -3
- package/CHANGELOG.md +55 -0
- package/CONTRIBUTING.md +6 -3
- package/README.md +30 -3
- package/README.zh-CN.md +30 -3
- package/SKILL.md +7 -1
- package/TESTING.md +1 -0
- package/chatwise-opencli.ps1 +82 -0
- package/dist/analysis.d.ts +38 -0
- package/dist/analysis.js +166 -0
- package/dist/browser/cdp.d.ts +0 -4
- package/dist/browser/cdp.js +53 -41
- package/dist/browser/cdp.test.d.ts +1 -0
- package/dist/browser/cdp.test.js +52 -0
- package/dist/browser/dom-snapshot.d.ts +2 -2
- package/dist/browser/dom-snapshot.js +54 -1
- package/dist/browser/dom-snapshot.test.js +36 -0
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/mcp.d.ts +0 -2
- package/dist/browser/mcp.js +2 -3
- package/dist/browser/page.d.ts +4 -3
- package/dist/browser/page.js +34 -37
- package/dist/browser/stealth.d.ts +0 -2
- package/dist/browser/stealth.js +24 -9
- package/dist/browser.test.js +2 -2
- package/dist/build-manifest.js +15 -9
- package/dist/build-manifest.test.js +12 -0
- package/dist/cascade.js +4 -2
- package/dist/cli-manifest.json +639 -258
- package/dist/cli.js +57 -29
- package/dist/clis/_shared/desktop-commands.d.ts +22 -0
- package/dist/clis/_shared/desktop-commands.js +108 -0
- package/dist/clis/antigravity/serve.js +5 -2
- package/dist/clis/arxiv/search.js +1 -1
- package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
- package/dist/clis/bilibili/dynamic.test.js +68 -0
- package/dist/clis/bilibili/favorite.js +4 -2
- package/dist/clis/bilibili/following.js +3 -2
- package/dist/clis/bilibili/subtitle.js +8 -7
- package/dist/clis/bilibili/utils.js +2 -2
- package/dist/clis/boss/batchgreet.js +1 -1
- package/dist/clis/boss/chatlist.js +1 -1
- package/dist/clis/boss/chatmsg.js +1 -1
- package/dist/clis/boss/detail.js +1 -1
- package/dist/clis/boss/exchange.js +1 -1
- package/dist/clis/boss/greet.js +1 -1
- package/dist/clis/boss/invite.js +1 -1
- package/dist/clis/boss/joblist.js +1 -1
- package/dist/clis/boss/mark.js +4 -3
- package/dist/clis/boss/recommend.js +1 -1
- package/dist/clis/boss/resume.js +1 -1
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +5 -4
- package/dist/clis/boss/stats.js +1 -1
- package/dist/clis/chatgpt/ask.js +4 -0
- package/dist/clis/chatgpt/new.js +5 -1
- package/dist/clis/chatgpt/read.js +5 -1
- package/dist/clis/chatgpt/send.js +2 -1
- package/dist/clis/chatgpt/status.js +5 -1
- package/dist/clis/chatwise/ask.js +8 -2
- package/dist/clis/chatwise/export.js +2 -0
- package/dist/clis/chatwise/history.js +2 -0
- package/dist/clis/chatwise/model.js +8 -3
- package/dist/clis/chatwise/new.js +3 -18
- package/dist/clis/chatwise/read.js +2 -0
- package/dist/clis/chatwise/screenshot.js +3 -27
- package/dist/clis/chatwise/send.js +8 -2
- package/dist/clis/chatwise/shared.d.ts +2 -0
- package/dist/clis/chatwise/shared.js +6 -0
- package/dist/clis/chatwise/status.js +3 -22
- package/dist/clis/codex/ask.js +6 -2
- package/dist/clis/codex/dump.js +2 -25
- package/dist/clis/codex/new.js +2 -25
- package/dist/clis/codex/screenshot.js +2 -27
- package/dist/clis/codex/send.js +6 -4
- package/dist/clis/codex/status.js +2 -22
- package/dist/clis/cursor/ask.js +2 -1
- package/dist/clis/cursor/composer.js +2 -1
- package/dist/clis/cursor/dump.js +2 -25
- package/dist/clis/cursor/new.js +2 -18
- package/dist/clis/cursor/read.js +2 -1
- package/dist/clis/cursor/screenshot.js +1 -30
- package/dist/clis/cursor/send.js +2 -1
- package/dist/clis/cursor/status.js +2 -21
- package/dist/clis/dictionary/examples.yaml +25 -0
- package/dist/clis/dictionary/search.yaml +27 -0
- package/dist/clis/dictionary/synonyms.yaml +25 -0
- package/dist/clis/douban/book-hot.js +1 -1
- package/dist/clis/douban/movie-hot.js +1 -1
- package/dist/clis/douban/search.js +1 -1
- package/dist/clis/douban/utils.d.ts +4 -1
- package/dist/clis/douban/utils.js +156 -1
- package/dist/clis/doubao/ask.js +1 -1
- package/dist/clis/doubao/new.js +1 -1
- package/dist/clis/doubao/read.js +1 -1
- package/dist/clis/doubao/send.js +1 -1
- package/dist/clis/doubao/status.js +1 -1
- package/dist/clis/doubao-app/ask.js +1 -1
- package/dist/clis/doubao-app/new.js +1 -1
- package/dist/clis/doubao-app/read.js +1 -1
- package/dist/clis/doubao-app/send.js +1 -1
- package/dist/clis/grok/ask.d.ts +4 -0
- package/dist/clis/grok/ask.js +28 -10
- package/dist/clis/grok/ask.test.js +18 -0
- package/dist/clis/jd/item.d.ts +1 -0
- package/dist/clis/jd/item.js +96 -0
- package/dist/clis/jd/item.test.d.ts +1 -0
- package/dist/clis/jd/item.test.js +28 -0
- package/dist/clis/jike/feed.js +1 -1
- package/dist/clis/jike/search.js +1 -1
- package/dist/clis/linkedin/search.js +5 -4
- package/dist/clis/linkedin/timeline.d.ts +21 -0
- package/dist/clis/linkedin/timeline.js +503 -0
- package/dist/clis/linkedin/timeline.test.d.ts +1 -0
- package/dist/clis/linkedin/timeline.test.js +81 -0
- package/dist/clis/medium/feed.js +1 -1
- package/dist/clis/medium/search.js +1 -1
- package/dist/clis/medium/user.js +1 -1
- package/dist/clis/medium/{shared.js → utils.js} +2 -1
- package/dist/clis/pixiv/detail.yaml +49 -0
- package/dist/clis/pixiv/download.d.ts +7 -0
- package/dist/clis/pixiv/download.js +78 -0
- package/dist/clis/pixiv/download.test.d.ts +1 -0
- package/dist/clis/pixiv/download.test.js +87 -0
- package/dist/clis/pixiv/illusts.d.ts +8 -0
- package/dist/clis/pixiv/illusts.js +65 -0
- package/dist/clis/pixiv/illusts.test.d.ts +1 -0
- package/dist/clis/pixiv/illusts.test.js +99 -0
- package/dist/clis/pixiv/ranking.yaml +53 -0
- package/dist/clis/pixiv/search.d.ts +6 -0
- package/dist/clis/pixiv/search.js +43 -0
- package/dist/clis/pixiv/search.test.d.ts +1 -0
- package/dist/clis/pixiv/search.test.js +83 -0
- package/dist/clis/pixiv/test-utils.d.ts +12 -0
- package/dist/clis/pixiv/test-utils.js +23 -0
- package/dist/clis/pixiv/user.yaml +46 -0
- package/dist/clis/pixiv/utils.d.ts +27 -0
- package/dist/clis/pixiv/utils.js +49 -0
- package/dist/clis/reddit/comment.js +2 -1
- package/dist/clis/reddit/read.js +4 -3
- package/dist/clis/reddit/read.test.d.ts +1 -0
- package/dist/clis/reddit/read.test.js +28 -0
- package/dist/clis/reddit/save.js +2 -1
- package/dist/clis/reddit/saved.js +7 -3
- package/dist/clis/reddit/subscribe.js +2 -1
- package/dist/clis/reddit/upvote.js +2 -1
- package/dist/clis/reddit/upvoted.js +7 -3
- package/dist/clis/sinablog/article.js +1 -1
- package/dist/clis/sinablog/hot.js +1 -1
- package/dist/clis/sinablog/user.js +1 -1
- package/dist/clis/substack/feed.js +1 -1
- package/dist/clis/substack/publication.js +1 -1
- package/dist/clis/substack/search.js +3 -2
- package/dist/clis/substack/{shared.js → utils.js} +3 -2
- package/dist/clis/tiktok/search.yaml +2 -1
- package/dist/clis/twitter/accept.js +2 -1
- package/dist/clis/twitter/article.js +4 -1
- package/dist/clis/twitter/block.js +2 -1
- package/dist/clis/twitter/bookmark.js +2 -1
- package/dist/clis/twitter/bookmarks.js +3 -2
- package/dist/clis/twitter/delete.js +2 -1
- package/dist/clis/twitter/follow.js +2 -1
- package/dist/clis/twitter/followers.js +3 -2
- package/dist/clis/twitter/following.js +3 -2
- package/dist/clis/twitter/hide-reply.js +2 -1
- package/dist/clis/twitter/like.js +2 -1
- package/dist/clis/twitter/notifications.js +2 -1
- package/dist/clis/twitter/post.js +2 -1
- package/dist/clis/twitter/profile.js +5 -2
- package/dist/clis/twitter/reply-dm.js +2 -1
- package/dist/clis/twitter/reply.js +2 -1
- package/dist/clis/twitter/search.js +30 -13
- package/dist/clis/twitter/search.test.d.ts +1 -0
- package/dist/clis/twitter/search.test.js +104 -0
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/timeline.js +3 -2
- package/dist/clis/twitter/trending.js +3 -2
- package/dist/clis/twitter/unblock.js +2 -1
- package/dist/clis/twitter/unbookmark.js +2 -1
- package/dist/clis/twitter/unfollow.js +2 -1
- package/dist/clis/v2ex/daily.js +3 -2
- package/dist/clis/v2ex/me.js +3 -2
- package/dist/clis/v2ex/notifications.js +4 -4
- package/dist/clis/web/read.d.ts +16 -0
- package/dist/clis/web/read.js +202 -0
- package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
- package/dist/clis/xueqiu/danjuan-utils.js +126 -0
- package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
- package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
- package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
- package/dist/clis/xueqiu/fund-holdings.js +28 -0
- package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
- package/dist/clis/xueqiu/fund-snapshot.js +25 -0
- package/dist/clis/youtube/transcript.js +5 -4
- package/dist/clis/youtube/video.js +3 -2
- package/dist/daemon.js +7 -3
- package/dist/discovery.js +11 -10
- package/dist/doctor.js +2 -1
- package/dist/download/index.d.ts +4 -12
- package/dist/download/index.js +33 -12
- package/dist/download/index.test.js +79 -2
- package/dist/download/media-download.js +4 -2
- package/dist/engine.test.js +76 -4
- package/dist/execution.d.ts +1 -9
- package/dist/execution.js +56 -46
- package/dist/explore.js +12 -111
- package/dist/external-clis.yaml +0 -8
- package/dist/external.js +7 -5
- package/dist/external.test.js +4 -0
- package/dist/generate.d.ts +0 -9
- package/dist/generate.js +4 -20
- package/dist/hooks.d.ts +46 -0
- package/dist/hooks.js +56 -0
- package/dist/hooks.test.d.ts +4 -0
- package/dist/hooks.test.js +92 -0
- package/dist/interceptor.js +70 -23
- package/dist/main.js +2 -0
- package/dist/output.js +12 -6
- package/dist/pipeline/executor.js +1 -1
- package/dist/pipeline/steps/browser.js +1 -3
- package/dist/pipeline/steps/download.js +42 -26
- package/dist/pipeline/steps/download.test.d.ts +1 -0
- package/dist/pipeline/steps/download.test.js +101 -0
- package/dist/pipeline/steps/fetch.js +40 -22
- package/dist/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/pipeline/steps/fetch.test.js +123 -0
- package/dist/pipeline/steps/transform.js +2 -6
- package/dist/pipeline/template.js +66 -52
- package/dist/pipeline/template.test.js +28 -0
- package/dist/pipeline/transform.test.js +18 -0
- package/dist/plugin.d.ts +40 -1
- package/dist/plugin.js +214 -17
- package/dist/plugin.test.d.ts +1 -1
- package/dist/plugin.test.js +219 -3
- package/dist/record.js +6 -98
- package/dist/registry-api.d.ts +2 -0
- package/dist/registry-api.js +1 -0
- package/dist/registry.d.ts +5 -2
- package/dist/registry.js +1 -2
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +14 -4
- package/dist/snapshotFormatter.d.ts +7 -14
- package/dist/snapshotFormatter.js +38 -78
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +29 -0
- package/dist/validate.js +3 -5
- package/dist/yaml-schema.d.ts +26 -0
- package/dist/yaml-schema.js +5 -0
- package/docs/.vitepress/config.mts +3 -0
- package/docs/adapters/browser/dictionary.md +27 -0
- package/docs/adapters/browser/jd.md +27 -0
- package/docs/adapters/browser/linkedin.md +6 -0
- package/docs/adapters/browser/pixiv.md +92 -0
- package/docs/adapters/browser/web.md +30 -0
- package/docs/adapters/browser/xueqiu.md +27 -9
- package/docs/adapters/index.md +3 -1
- package/docs/comparison.md +125 -0
- package/docs/developer/contributing.md +21 -2
- package/docs/developer/testing.md +14 -8
- package/docs/developer/ts-adapter.md +18 -0
- package/docs/developer/yaml-adapter.md +16 -0
- package/docs/guide/plugins.md +10 -0
- package/docs/zh/guide/plugins.md +10 -0
- package/extension/dist/background.js +519 -444
- package/extension/manifest.json +1 -1
- package/extension/package.json +1 -1
- package/extension/src/background.test.ts +46 -1
- package/extension/src/background.ts +108 -33
- package/extension/src/cdp.ts +9 -9
- package/package.json +3 -2
- package/scripts/check-doc-coverage.sh +2 -0
- package/src/analysis.ts +170 -0
- package/src/browser/cdp.test.ts +66 -0
- package/src/browser/cdp.ts +59 -44
- package/src/browser/dom-snapshot.test.ts +42 -0
- package/src/browser/dom-snapshot.ts +56 -3
- package/src/browser/index.ts +2 -2
- package/src/browser/mcp.ts +2 -4
- package/src/browser/page.ts +34 -37
- package/src/browser/stealth.ts +24 -10
- package/src/browser.test.ts +2 -2
- package/src/build-manifest.test.ts +14 -0
- package/src/build-manifest.ts +13 -31
- package/src/cascade.ts +5 -3
- package/src/cli.ts +66 -34
- package/src/clis/_shared/desktop-commands.ts +121 -0
- package/src/clis/antigravity/serve.ts +6 -3
- package/src/clis/arxiv/search.ts +1 -1
- package/src/clis/bilibili/dynamic.test.ts +79 -0
- package/src/clis/bilibili/favorite.ts +5 -2
- package/src/clis/bilibili/following.ts +3 -2
- package/src/clis/bilibili/subtitle.ts +8 -7
- package/src/clis/bilibili/utils.ts +2 -2
- package/src/clis/boss/batchgreet.ts +1 -1
- package/src/clis/boss/chatlist.ts +1 -1
- package/src/clis/boss/chatmsg.ts +1 -1
- package/src/clis/boss/detail.ts +1 -1
- package/src/clis/boss/exchange.ts +1 -1
- package/src/clis/boss/greet.ts +1 -1
- package/src/clis/boss/invite.ts +1 -1
- package/src/clis/boss/joblist.ts +1 -1
- package/src/clis/boss/mark.ts +4 -3
- package/src/clis/boss/recommend.ts +1 -1
- package/src/clis/boss/resume.ts +1 -1
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +5 -4
- package/src/clis/boss/stats.ts +1 -1
- package/src/clis/chatgpt/ask.ts +5 -0
- package/src/clis/chatgpt/new.ts +7 -2
- package/src/clis/chatgpt/read.ts +7 -2
- package/src/clis/chatgpt/send.ts +3 -2
- package/src/clis/chatgpt/status.ts +6 -1
- package/src/clis/chatwise/ask.ts +7 -2
- package/src/clis/chatwise/export.ts +2 -0
- package/src/clis/chatwise/history.ts +2 -0
- package/src/clis/chatwise/model.ts +7 -3
- package/src/clis/chatwise/new.ts +3 -20
- package/src/clis/chatwise/read.ts +2 -0
- package/src/clis/chatwise/screenshot.ts +3 -32
- package/src/clis/chatwise/send.ts +7 -2
- package/src/clis/chatwise/shared.ts +8 -0
- package/src/clis/chatwise/status.ts +3 -24
- package/src/clis/codex/ask.ts +5 -2
- package/src/clis/codex/dump.ts +2 -27
- package/src/clis/codex/new.ts +2 -28
- package/src/clis/codex/screenshot.ts +2 -32
- package/src/clis/codex/send.ts +5 -4
- package/src/clis/codex/status.ts +2 -24
- package/src/clis/cursor/ask.ts +2 -1
- package/src/clis/cursor/composer.ts +2 -1
- package/src/clis/cursor/dump.ts +2 -27
- package/src/clis/cursor/new.ts +2 -20
- package/src/clis/cursor/read.ts +2 -1
- package/src/clis/cursor/screenshot.ts +1 -36
- package/src/clis/cursor/send.ts +2 -1
- package/src/clis/cursor/status.ts +2 -22
- package/src/clis/dictionary/examples.yaml +25 -0
- package/src/clis/dictionary/search.yaml +27 -0
- package/src/clis/dictionary/synonyms.yaml +25 -0
- package/src/clis/douban/book-hot.ts +1 -1
- package/src/clis/douban/movie-hot.ts +1 -1
- package/src/clis/douban/search.ts +1 -1
- package/src/clis/douban/utils.ts +165 -1
- package/src/clis/doubao/ask.ts +1 -1
- package/src/clis/doubao/new.ts +1 -1
- package/src/clis/doubao/read.ts +1 -1
- package/src/clis/doubao/send.ts +1 -1
- package/src/clis/doubao/status.ts +1 -1
- package/src/clis/doubao-app/ask.ts +1 -1
- package/src/clis/doubao-app/new.ts +1 -1
- package/src/clis/doubao-app/read.ts +1 -1
- package/src/clis/doubao-app/send.ts +1 -1
- package/src/clis/grok/ask.test.ts +25 -0
- package/src/clis/grok/ask.ts +25 -12
- package/src/clis/jd/item.test.ts +35 -0
- package/src/clis/jd/item.ts +101 -0
- package/src/clis/jike/feed.ts +1 -1
- package/src/clis/jike/search.ts +1 -1
- package/src/clis/linkedin/search.ts +5 -4
- package/src/clis/linkedin/timeline.test.ts +99 -0
- package/src/clis/linkedin/timeline.ts +532 -0
- package/src/clis/medium/feed.ts +1 -1
- package/src/clis/medium/search.ts +1 -1
- package/src/clis/medium/user.ts +1 -1
- package/src/clis/medium/{shared.ts → utils.ts} +2 -1
- package/src/clis/pixiv/detail.yaml +49 -0
- package/src/clis/pixiv/download.test.ts +114 -0
- package/src/clis/pixiv/download.ts +91 -0
- package/src/clis/pixiv/illusts.test.ts +115 -0
- package/src/clis/pixiv/illusts.ts +78 -0
- package/src/clis/pixiv/ranking.yaml +53 -0
- package/src/clis/pixiv/search.test.ts +97 -0
- package/src/clis/pixiv/search.ts +53 -0
- package/src/clis/pixiv/test-utils.ts +29 -0
- package/src/clis/pixiv/user.yaml +46 -0
- package/src/clis/pixiv/utils.ts +62 -0
- package/src/clis/reddit/comment.ts +2 -1
- package/src/clis/reddit/read.test.ts +34 -0
- package/src/clis/reddit/read.ts +4 -3
- package/src/clis/reddit/save.ts +2 -1
- package/src/clis/reddit/saved.ts +6 -2
- package/src/clis/reddit/subscribe.ts +2 -1
- package/src/clis/reddit/upvote.ts +2 -1
- package/src/clis/reddit/upvoted.ts +6 -2
- package/src/clis/sinablog/article.ts +1 -1
- package/src/clis/sinablog/hot.ts +1 -1
- package/src/clis/sinablog/user.ts +1 -1
- package/src/clis/substack/feed.ts +1 -1
- package/src/clis/substack/publication.ts +1 -1
- package/src/clis/substack/search.ts +3 -2
- package/src/clis/substack/{shared.ts → utils.ts} +3 -2
- package/src/clis/tiktok/search.yaml +2 -1
- package/src/clis/twitter/accept.ts +2 -1
- package/src/clis/twitter/article.ts +3 -1
- package/src/clis/twitter/block.ts +2 -1
- package/src/clis/twitter/bookmark.ts +2 -1
- package/src/clis/twitter/bookmarks.ts +3 -2
- package/src/clis/twitter/delete.ts +2 -1
- package/src/clis/twitter/follow.ts +2 -1
- package/src/clis/twitter/followers.ts +3 -2
- package/src/clis/twitter/following.ts +3 -2
- package/src/clis/twitter/hide-reply.ts +2 -1
- package/src/clis/twitter/like.ts +2 -1
- package/src/clis/twitter/notifications.ts +2 -1
- package/src/clis/twitter/post.ts +2 -1
- package/src/clis/twitter/profile.ts +4 -2
- package/src/clis/twitter/reply-dm.ts +2 -1
- package/src/clis/twitter/reply.ts +2 -1
- package/src/clis/twitter/search.test.ts +113 -0
- package/src/clis/twitter/search.ts +38 -14
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/timeline.ts +3 -2
- package/src/clis/twitter/trending.ts +3 -2
- package/src/clis/twitter/unblock.ts +2 -1
- package/src/clis/twitter/unbookmark.ts +2 -1
- package/src/clis/twitter/unfollow.ts +2 -1
- package/src/clis/v2ex/daily.ts +3 -2
- package/src/clis/v2ex/me.ts +3 -2
- package/src/clis/v2ex/notifications.ts +3 -4
- package/src/clis/web/read.ts +210 -0
- package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
- package/src/clis/xueqiu/danjuan-utils.ts +176 -0
- package/src/clis/xueqiu/fund-holdings.ts +32 -0
- package/src/clis/xueqiu/fund-snapshot.ts +27 -0
- package/src/clis/youtube/transcript.ts +5 -4
- package/src/clis/youtube/video.ts +3 -2
- package/src/daemon.ts +5 -4
- package/src/discovery.ts +12 -34
- package/src/doctor.ts +3 -2
- package/src/download/index.test.ts +93 -2
- package/src/download/index.ts +44 -23
- package/src/download/media-download.ts +5 -3
- package/src/engine.test.ts +84 -3
- package/src/execution.ts +62 -46
- package/src/explore.ts +21 -90
- package/src/external-clis.yaml +0 -8
- package/src/external.test.ts +9 -0
- package/src/external.ts +12 -10
- package/src/generate.ts +4 -41
- package/src/hooks.test.ts +126 -0
- package/src/hooks.ts +90 -0
- package/src/interceptor.ts +73 -23
- package/src/main.ts +2 -0
- package/src/output.ts +14 -6
- package/src/pipeline/executor.ts +1 -1
- package/src/pipeline/steps/browser.ts +1 -3
- package/src/pipeline/steps/download.test.ts +136 -0
- package/src/pipeline/steps/download.ts +47 -34
- package/src/pipeline/steps/fetch.test.ts +179 -0
- package/src/pipeline/steps/fetch.ts +39 -23
- package/src/pipeline/steps/transform.ts +2 -6
- package/src/pipeline/template.test.ts +28 -0
- package/src/pipeline/template.ts +67 -79
- package/src/pipeline/transform.test.ts +20 -0
- package/src/plugin.test.ts +251 -3
- package/src/plugin.ts +265 -21
- package/src/record.ts +12 -84
- package/src/registry-api.ts +2 -0
- package/src/registry.ts +7 -4
- package/src/runtime.ts +14 -4
- package/src/snapshotFormatter.ts +43 -121
- package/src/utils.ts +39 -0
- package/src/validate.ts +3 -5
- package/src/yaml-schema.ts +28 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
- package/tests/e2e/plugin-management.test.ts +137 -0
- package/tests/e2e/public-commands.test.ts +34 -1
- package/vitest.config.ts +19 -1
- package/.github/workflows/pkg-pr-new.yml +0 -30
- package/dist/clis/douban/shared.d.ts +0 -4
- package/dist/clis/douban/shared.js +0 -155
- package/src/clis/douban/shared.ts +0 -165
- /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/boss/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
- /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/jike/{shared.js → utils.js} +0 -0
- /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
- /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
- /package/src/clis/boss/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
- /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
- /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
package/src/clis/douban/utils.ts
CHANGED
|
@@ -1,9 +1,173 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Douban
|
|
2
|
+
* Douban adapter utilities.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { CliError } from '../../errors.js';
|
|
5
6
|
import type { IPage } from '../../types.js';
|
|
6
7
|
|
|
8
|
+
function clampLimit(limit: number): number {
|
|
9
|
+
return Math.max(1, Math.min(limit || 20, 50));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function ensureDoubanReady(page: IPage): Promise<void> {
|
|
13
|
+
const state = await page.evaluate(`
|
|
14
|
+
(() => {
|
|
15
|
+
const title = (document.title || '').trim();
|
|
16
|
+
const href = (location.href || '').trim();
|
|
17
|
+
const blocked = href.includes('sec.douban.com') || /登录跳转/.test(title) || /异常请求/.test(document.body?.innerText || '');
|
|
18
|
+
return { blocked, title, href };
|
|
19
|
+
})()
|
|
20
|
+
`);
|
|
21
|
+
if (state?.blocked) {
|
|
22
|
+
throw new CliError(
|
|
23
|
+
'AUTH_REQUIRED',
|
|
24
|
+
'Douban requires a logged-in browser session before these commands can load data.',
|
|
25
|
+
'Please sign in to douban.com in the browser that opencli reuses, then rerun the command.',
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function loadDoubanBookHot(page: IPage, limit: number): Promise<any[]> {
|
|
31
|
+
const safeLimit = clampLimit(limit);
|
|
32
|
+
await page.goto('https://book.douban.com/chart');
|
|
33
|
+
await page.wait(4);
|
|
34
|
+
await ensureDoubanReady(page);
|
|
35
|
+
const data = await page.evaluate(`
|
|
36
|
+
(() => {
|
|
37
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
38
|
+
const books = [];
|
|
39
|
+
for (const el of Array.from(document.querySelectorAll('.media.clearfix'))) {
|
|
40
|
+
try {
|
|
41
|
+
const titleEl = el.querySelector('h2 a[href*="/subject/"]');
|
|
42
|
+
const title = normalize(titleEl?.textContent);
|
|
43
|
+
let url = titleEl?.getAttribute('href') || '';
|
|
44
|
+
if (!title || !url) continue;
|
|
45
|
+
if (!url.startsWith('http')) url = 'https://book.douban.com' + url;
|
|
46
|
+
|
|
47
|
+
const info = normalize(el.querySelector('.subject-abstract, .pl, .pub')?.textContent);
|
|
48
|
+
const infoParts = info.split('/').map((part) => part.trim()).filter(Boolean);
|
|
49
|
+
const ratingText = normalize(el.querySelector('.subject-rating .font-small, .rating_nums, .rating')?.textContent);
|
|
50
|
+
const quote = Array.from(el.querySelectorAll('.subject-tags .tag'))
|
|
51
|
+
.map((node) => normalize(node.textContent))
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(' / ');
|
|
54
|
+
|
|
55
|
+
books.push({
|
|
56
|
+
rank: parseInt(normalize(el.querySelector('.green-num-box')?.textContent), 10) || books.length + 1,
|
|
57
|
+
title,
|
|
58
|
+
rating: parseFloat(ratingText) || 0,
|
|
59
|
+
quote,
|
|
60
|
+
author: infoParts[0] || '',
|
|
61
|
+
publisher: infoParts.find((part) => /出版社|出版公司|Press/i.test(part)) || infoParts[2] || '',
|
|
62
|
+
year: infoParts.find((part) => /\\d{4}(?:-\\d{1,2})?/.test(part))?.match(/\\d{4}/)?.[0] || '',
|
|
63
|
+
price: infoParts.find((part) => /元|USD|\\$|¥/.test(part)) || '',
|
|
64
|
+
url,
|
|
65
|
+
cover: el.querySelector('img')?.getAttribute('src') || '',
|
|
66
|
+
});
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
return books.slice(0, ${safeLimit});
|
|
70
|
+
})()
|
|
71
|
+
`);
|
|
72
|
+
return Array.isArray(data) ? data : [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadDoubanMovieHot(page: IPage, limit: number): Promise<any[]> {
|
|
76
|
+
const safeLimit = clampLimit(limit);
|
|
77
|
+
await page.goto('https://movie.douban.com/chart');
|
|
78
|
+
await page.wait(4);
|
|
79
|
+
await ensureDoubanReady(page);
|
|
80
|
+
const data = await page.evaluate(`
|
|
81
|
+
(() => {
|
|
82
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
83
|
+
const results = [];
|
|
84
|
+
for (const el of Array.from(document.querySelectorAll('.item'))) {
|
|
85
|
+
const titleEl = el.querySelector('.pl2 a');
|
|
86
|
+
const title = normalize(titleEl?.textContent);
|
|
87
|
+
let url = titleEl?.getAttribute('href') || '';
|
|
88
|
+
if (!title || !url) continue;
|
|
89
|
+
if (!url.startsWith('http')) url = 'https://movie.douban.com' + url;
|
|
90
|
+
|
|
91
|
+
const info = normalize(el.querySelector('.pl2 p')?.textContent);
|
|
92
|
+
const infoParts = info.split('/').map((part) => part.trim()).filter(Boolean);
|
|
93
|
+
const releaseIndex = (() => {
|
|
94
|
+
for (let i = infoParts.length - 1; i >= 0; i -= 1) {
|
|
95
|
+
if (/\\d{4}-\\d{2}-\\d{2}|\\d{4}\\/\\d{2}\\/\\d{2}/.test(infoParts[i])) return i;
|
|
96
|
+
}
|
|
97
|
+
return -1;
|
|
98
|
+
})();
|
|
99
|
+
const directorPart = releaseIndex >= 1 ? infoParts[releaseIndex - 1] : '';
|
|
100
|
+
const regionPart = releaseIndex >= 2 ? infoParts[releaseIndex - 2] : '';
|
|
101
|
+
const yearMatch = info.match(/\\b(19|20)\\d{2}\\b/);
|
|
102
|
+
results.push({
|
|
103
|
+
rank: results.length + 1,
|
|
104
|
+
title,
|
|
105
|
+
rating: parseFloat(normalize(el.querySelector('.rating_nums')?.textContent)) || 0,
|
|
106
|
+
quote: normalize(el.querySelector('.inq')?.textContent),
|
|
107
|
+
director: directorPart.replace(/^导演:\\s*/, ''),
|
|
108
|
+
year: yearMatch?.[0] || '',
|
|
109
|
+
region: regionPart,
|
|
110
|
+
url,
|
|
111
|
+
cover: el.querySelector('img')?.getAttribute('src') || '',
|
|
112
|
+
});
|
|
113
|
+
if (results.length >= ${safeLimit}) break;
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
})()
|
|
117
|
+
`);
|
|
118
|
+
return Array.isArray(data) ? data : [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function searchDouban(page: IPage, type: string, keyword: string, limit: number): Promise<any[]> {
|
|
122
|
+
const safeLimit = clampLimit(limit);
|
|
123
|
+
await page.goto(`https://search.douban.com/${encodeURIComponent(type)}/subject_search?search_text=${encodeURIComponent(keyword)}`);
|
|
124
|
+
await page.wait(2);
|
|
125
|
+
await ensureDoubanReady(page);
|
|
126
|
+
const data = await page.evaluate(`
|
|
127
|
+
(async () => {
|
|
128
|
+
const type = ${JSON.stringify(type)};
|
|
129
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < 20; i += 1) {
|
|
134
|
+
if (document.querySelector('.item-root .title-text, .item-root .title a')) break;
|
|
135
|
+
await sleep(300);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const items = Array.from(document.querySelectorAll('.item-root'));
|
|
139
|
+
|
|
140
|
+
const results = [];
|
|
141
|
+
for (const el of items) {
|
|
142
|
+
const titleEl = el.querySelector('.title-text, .title a, a[title]');
|
|
143
|
+
const title = normalize(titleEl?.textContent) || normalize(titleEl?.getAttribute('title'));
|
|
144
|
+
let url = titleEl?.getAttribute('href') || '';
|
|
145
|
+
if (!title || !url) continue;
|
|
146
|
+
if (!url.startsWith('http')) url = 'https://search.douban.com' + url;
|
|
147
|
+
if (!url.includes('/subject/') || seen.has(url)) continue;
|
|
148
|
+
seen.add(url);
|
|
149
|
+
const ratingText = normalize(el.querySelector('.rating_nums')?.textContent);
|
|
150
|
+
const abstract = normalize(
|
|
151
|
+
el.querySelector('.meta.abstract, .meta, .abstract, p')?.textContent,
|
|
152
|
+
);
|
|
153
|
+
results.push({
|
|
154
|
+
rank: results.length + 1,
|
|
155
|
+
id: url.match(/subject\\/(\\d+)/)?.[1] || '',
|
|
156
|
+
type,
|
|
157
|
+
title,
|
|
158
|
+
rating: ratingText.includes('.') ? parseFloat(ratingText) : 0,
|
|
159
|
+
abstract: abstract.slice(0, 100) + (abstract.length > 100 ? '...' : ''),
|
|
160
|
+
url,
|
|
161
|
+
cover: el.querySelector('img')?.getAttribute('src') || '',
|
|
162
|
+
});
|
|
163
|
+
if (results.length >= ${safeLimit}) break;
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
})()
|
|
167
|
+
`);
|
|
168
|
+
return Array.isArray(data) ? data : [];
|
|
169
|
+
}
|
|
170
|
+
|
|
7
171
|
/**
|
|
8
172
|
* Get current user's Douban ID from movie.douban.com/mine page
|
|
9
173
|
*/
|
package/src/clis/doubao/ask.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import { DOUBAO_DOMAIN, getDoubaoTranscriptLines, getDoubaoVisibleTurns, sendDoubaoMessage, waitForDoubaoResponse } from './
|
|
3
|
+
import { DOUBAO_DOMAIN, getDoubaoTranscriptLines, getDoubaoVisibleTurns, sendDoubaoMessage, waitForDoubaoResponse } from './utils.js';
|
|
4
4
|
|
|
5
5
|
export const askCommand = cli({
|
|
6
6
|
site: 'doubao',
|
package/src/clis/doubao/new.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import { DOUBAO_DOMAIN, DOUBAO_CHAT_URL, startNewDoubaoChat } from './
|
|
3
|
+
import { DOUBAO_DOMAIN, DOUBAO_CHAT_URL, startNewDoubaoChat } from './utils.js';
|
|
4
4
|
|
|
5
5
|
export const newCommand = cli({
|
|
6
6
|
site: 'doubao',
|
package/src/clis/doubao/read.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import { DOUBAO_DOMAIN, getDoubaoVisibleTurns } from './
|
|
3
|
+
import { DOUBAO_DOMAIN, getDoubaoVisibleTurns } from './utils.js';
|
|
4
4
|
|
|
5
5
|
export const readCommand = cli({
|
|
6
6
|
site: 'doubao',
|
package/src/clis/doubao/send.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import { DOUBAO_DOMAIN, DOUBAO_CHAT_URL, sendDoubaoMessage } from './
|
|
3
|
+
import { DOUBAO_DOMAIN, DOUBAO_CHAT_URL, sendDoubaoMessage } from './utils.js';
|
|
4
4
|
|
|
5
5
|
export const sendCommand = cli({
|
|
6
6
|
site: 'doubao',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import { DOUBAO_DOMAIN, DOUBAO_CHAT_URL, getDoubaoPageState } from './
|
|
3
|
+
import { DOUBAO_DOMAIN, DOUBAO_CHAT_URL, getDoubaoPageState } from './utils.js';
|
|
4
4
|
|
|
5
5
|
export const statusCommand = cli({
|
|
6
6
|
site: 'doubao',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import { SEL, injectTextScript, clickSendScript, pollResponseScript } from './
|
|
2
|
+
import { SEL, injectTextScript, clickSendScript, pollResponseScript } from './utils.js';
|
|
3
3
|
|
|
4
4
|
export const askCommand = cli({
|
|
5
5
|
site: 'doubao-app',
|
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
2
3
|
import { __test__ } from './ask.js';
|
|
3
4
|
|
|
4
5
|
describe('grok ask helpers', () => {
|
|
6
|
+
describe('isOnGrok', () => {
|
|
7
|
+
const fakePage = (url: string | Error): IPage =>
|
|
8
|
+
({ evaluate: () => url instanceof Error ? Promise.reject(url) : Promise.resolve(url) }) as unknown as IPage;
|
|
9
|
+
|
|
10
|
+
it('returns true for grok.com URLs', async () => {
|
|
11
|
+
expect(await __test__.isOnGrok(fakePage('https://grok.com/'))).toBe(true);
|
|
12
|
+
expect(await __test__.isOnGrok(fakePage('https://grok.com/chat/abc123'))).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns true for grok.com subdomains', async () => {
|
|
16
|
+
expect(await __test__.isOnGrok(fakePage('https://api.grok.com/v1'))).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns false for non-grok domains', async () => {
|
|
20
|
+
expect(await __test__.isOnGrok(fakePage('https://fakegrok.com/'))).toBe(false);
|
|
21
|
+
expect(await __test__.isOnGrok(fakePage('https://example.com/?next=grok.com'))).toBe(false);
|
|
22
|
+
expect(await __test__.isOnGrok(fakePage('about:blank'))).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns false when evaluate throws (detached tab)', async () => {
|
|
26
|
+
expect(await __test__.isOnGrok(fakePage(new Error('detached')))).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
5
30
|
it('normalizes boolean flags for explicit web routing', () => {
|
|
6
31
|
expect(__test__.normalizeBooleanFlag(true)).toBe(true);
|
|
7
32
|
expect(__test__.normalizeBooleanFlag('true')).toBe(true);
|
package/src/clis/grok/ask.ts
CHANGED
|
@@ -53,6 +53,19 @@ function updateStableState(previousText: string, stableCount: number, nextText:
|
|
|
53
53
|
return { previousText: nextText, stableCount: 0 };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** Check whether the tab is already on grok.com (any path). */
|
|
57
|
+
async function isOnGrok(page: IPage): Promise<boolean> {
|
|
58
|
+
// catch handles blank tabs (about:blank) or detached pages
|
|
59
|
+
const url = await page.evaluate('window.location.href').catch(() => '');
|
|
60
|
+
if (typeof url !== 'string' || !url) return false;
|
|
61
|
+
try {
|
|
62
|
+
const hostname = new URL(url).hostname;
|
|
63
|
+
return hostname === 'grok.com' || hostname.endsWith('.grok.com');
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
56
69
|
async function runDefaultAsk(
|
|
57
70
|
page: IPage,
|
|
58
71
|
prompt: string,
|
|
@@ -60,21 +73,17 @@ async function runDefaultAsk(
|
|
|
60
73
|
newChat: boolean,
|
|
61
74
|
) {
|
|
62
75
|
if (newChat) {
|
|
76
|
+
// Explicitly start a fresh conversation via the homepage
|
|
63
77
|
await page.goto(GROK_URL);
|
|
64
78
|
await page.wait(2);
|
|
65
|
-
await page
|
|
66
|
-
const btn = [...document.querySelectorAll('a, button')].find(b => {
|
|
67
|
-
const t = (b.textContent || '').trim().toLowerCase();
|
|
68
|
-
return t.includes('new') || b.getAttribute('href') === '/';
|
|
69
|
-
});
|
|
70
|
-
if (btn) btn.click();
|
|
71
|
-
})()`);
|
|
79
|
+
await tryStartFreshChat(page);
|
|
72
80
|
await page.wait(2);
|
|
81
|
+
} else if (!(await isOnGrok(page))) {
|
|
82
|
+
// First invocation or tab was recycled — navigate to Grok
|
|
83
|
+
await page.goto(GROK_URL);
|
|
84
|
+
await page.wait(3);
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
await page.goto(GROK_URL);
|
|
76
|
-
await page.wait(3);
|
|
77
|
-
|
|
78
87
|
const promptJson = JSON.stringify(prompt);
|
|
79
88
|
const sendResult = await page.evaluate(`(async () => {
|
|
80
89
|
try {
|
|
@@ -249,11 +258,14 @@ async function runExplicitWebAsk(
|
|
|
249
258
|
timeoutMs: number,
|
|
250
259
|
newChat: boolean,
|
|
251
260
|
) {
|
|
252
|
-
await page.goto(GROK_URL, { settleMs: 2000 });
|
|
253
|
-
|
|
254
261
|
if (newChat) {
|
|
262
|
+
// Navigate to homepage and start a fresh conversation
|
|
263
|
+
await page.goto(GROK_URL, { settleMs: 2000 });
|
|
255
264
|
await tryStartFreshChat(page);
|
|
256
265
|
await page.wait(2);
|
|
266
|
+
} else if (!(await isOnGrok(page))) {
|
|
267
|
+
// First invocation or tab was recycled — navigate to Grok
|
|
268
|
+
await page.goto(GROK_URL, { settleMs: 2000 });
|
|
257
269
|
}
|
|
258
270
|
|
|
259
271
|
const baselineBubbles = await getBubbleTexts(page);
|
|
@@ -318,4 +330,5 @@ export const __test__ = {
|
|
|
318
330
|
updateStableState,
|
|
319
331
|
normalizeBooleanFlag,
|
|
320
332
|
normalizeBubbleText,
|
|
333
|
+
isOnGrok,
|
|
321
334
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './item.js';
|
|
4
|
+
|
|
5
|
+
describe('jd item adapter', () => {
|
|
6
|
+
const command = getRegistry().get('jd/item');
|
|
7
|
+
|
|
8
|
+
it('registers the command with correct shape', () => {
|
|
9
|
+
expect(command).toBeDefined();
|
|
10
|
+
expect(command!.site).toBe('jd');
|
|
11
|
+
expect(command!.name).toBe('item');
|
|
12
|
+
expect(command!.domain).toBe('item.jd.com');
|
|
13
|
+
expect(command!.strategy).toBe('cookie');
|
|
14
|
+
expect(typeof command!.func).toBe('function');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('has sku as a required positional arg', () => {
|
|
18
|
+
const skuArg = command!.args.find((a) => a.name === 'sku');
|
|
19
|
+
expect(skuArg).toBeDefined();
|
|
20
|
+
expect(skuArg!.required).toBe(true);
|
|
21
|
+
expect(skuArg!.positional).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('has images arg with default 10', () => {
|
|
25
|
+
const imagesArg = command!.args.find((a) => a.name === 'images');
|
|
26
|
+
expect(imagesArg).toBeDefined();
|
|
27
|
+
expect(imagesArg!.default).toBe(10);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('includes expected columns', () => {
|
|
31
|
+
expect(command!.columns).toEqual(
|
|
32
|
+
expect.arrayContaining(['title', 'price', 'shop', 'specs', 'mainImages', 'detailImages']),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 京东商品详情 — browser cookie, DOM scraping + evaluate.
|
|
3
|
+
*
|
|
4
|
+
* 依赖: 需要在 Chrome 已登录京东
|
|
5
|
+
* 用法: opencli jd item 100291143898
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
|
|
9
|
+
cli({
|
|
10
|
+
site: 'jd',
|
|
11
|
+
name: 'item',
|
|
12
|
+
description: '京东商品详情(价格、主图、详情图、规格参数)',
|
|
13
|
+
domain: 'item.jd.com',
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{
|
|
17
|
+
name: 'sku',
|
|
18
|
+
required: true,
|
|
19
|
+
positional: true,
|
|
20
|
+
help: '商品 SKU ID(如 100291143898)',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'images',
|
|
24
|
+
type: 'int',
|
|
25
|
+
default: 10,
|
|
26
|
+
help: '详情图数量(默认10)',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
columns: ['title', 'price', 'shop', 'specs', 'mainImages', 'detailImages'],
|
|
30
|
+
func: async (page, kwargs) => {
|
|
31
|
+
const sku = kwargs.sku;
|
|
32
|
+
const maxImages = kwargs.images as number;
|
|
33
|
+
const url = `https://item.jd.com/${sku}.html`;
|
|
34
|
+
|
|
35
|
+
await page.goto(url, { waitUntil: 'load' });
|
|
36
|
+
await page.wait(2);
|
|
37
|
+
|
|
38
|
+
// 滚动加载详情图
|
|
39
|
+
for (let i = 0; i < 6; i++) {
|
|
40
|
+
await page.evaluate(`window.scrollTo(0, ${i * 2500})`);
|
|
41
|
+
await page.wait(1);
|
|
42
|
+
}
|
|
43
|
+
await page.evaluate(`window.scrollTo(0, document.body.scrollHeight)`);
|
|
44
|
+
await page.wait(2);
|
|
45
|
+
|
|
46
|
+
const data = await page.evaluate(`
|
|
47
|
+
(() => {
|
|
48
|
+
const maxImg = ${maxImages};
|
|
49
|
+
// 尝试多种价格选择器
|
|
50
|
+
const skuMatch = location.pathname.match(/(\\d+)\\.html/);
|
|
51
|
+
const sku = skuMatch ? skuMatch[1] : '';
|
|
52
|
+
const priceEl = document.querySelector('.J-p-' + sku) ||
|
|
53
|
+
document.querySelector('[class*="price"] [class*="num"]') ||
|
|
54
|
+
document.querySelector('.p-price strong') ||
|
|
55
|
+
document.querySelector('.price.jd-price');
|
|
56
|
+
const price = priceEl?.textContent?.trim() || 'not found';
|
|
57
|
+
|
|
58
|
+
// 标题
|
|
59
|
+
const title = document.querySelector('.product-title')?.textContent?.trim() ||
|
|
60
|
+
document.title.split('-')[0].trim();
|
|
61
|
+
|
|
62
|
+
// 店铺
|
|
63
|
+
const shop = document.querySelector('.J-shop-name')?.textContent?.trim() || '京东自营';
|
|
64
|
+
|
|
65
|
+
// 所有图片
|
|
66
|
+
const allImgs = Array.from(document.querySelectorAll('img[src*="360buyimg.com"]'));
|
|
67
|
+
const srcs = allImgs.map(img => img.src).filter(Boolean);
|
|
68
|
+
const unique = [...new Set(srcs)];
|
|
69
|
+
|
|
70
|
+
// 主图
|
|
71
|
+
const mainImgs = unique
|
|
72
|
+
.filter(u => u.includes('/n1/') || u.includes('/n3/') || u.includes('/n4/') || u.includes('/img/'))
|
|
73
|
+
.slice(0, maxImg);
|
|
74
|
+
|
|
75
|
+
// 详情图
|
|
76
|
+
const detailImgs = unique
|
|
77
|
+
.filter(u => u.includes('/babel/') || u.includes('/popshop/'))
|
|
78
|
+
.slice(0, maxImg);
|
|
79
|
+
|
|
80
|
+
// 规格参数:从页面文本提取
|
|
81
|
+
const text = document.body.innerText;
|
|
82
|
+
const specMatch = text.match(/商品编号[\\s\\S]*?(?=包装清单|\\n\\n|$)/);
|
|
83
|
+
let specs = {};
|
|
84
|
+
if (specMatch) {
|
|
85
|
+
const lines = specMatch[0].split('\\n').filter(l => l.trim());
|
|
86
|
+
for (let i = 0; i < lines.length - 1; i += 2) {
|
|
87
|
+
const key = lines[i].trim();
|
|
88
|
+
const val = lines[i + 1]?.trim() || '';
|
|
89
|
+
if (key && val && key !== '商品编号') {
|
|
90
|
+
specs[key] = val;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { title, price, shop, specs, mainImages: mainImgs, detailImages: detailImgs, totalImages: unique.length };
|
|
96
|
+
})()
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
return [data];
|
|
100
|
+
},
|
|
101
|
+
});
|
package/src/clis/jike/feed.ts
CHANGED
package/src/clis/jike/search.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
+
import { ArgumentError, CommandExecutionError } from '../../errors.js';
|
|
3
4
|
|
|
4
5
|
// ── Filter value mappings ──────────────────────────────────────────────
|
|
5
6
|
|
|
@@ -64,7 +65,7 @@ function mapFilterValues(input: unknown, mapping: Record<string, string>, label:
|
|
|
64
65
|
const resolved = values.map(value => {
|
|
65
66
|
const key = value.toLowerCase();
|
|
66
67
|
const mapped = mapping[key];
|
|
67
|
-
if (!mapped) throw new
|
|
68
|
+
if (!mapped) throw new ArgumentError(`Unsupported ${label}: ${value}`);
|
|
68
69
|
return mapped;
|
|
69
70
|
});
|
|
70
71
|
return [...new Set(resolved)];
|
|
@@ -214,7 +215,7 @@ async function resolveCompanyIds(page: IPage, input: unknown): Promise<string[]>
|
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
if (unresolved.length) {
|
|
217
|
-
throw new
|
|
218
|
+
throw new ArgumentError(`Could not resolve LinkedIn company filter: ${unresolved.join(', ')}`);
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
return [...ids];
|
|
@@ -252,7 +253,7 @@ async function fetchJobCards(
|
|
|
252
253
|
})()`);
|
|
253
254
|
|
|
254
255
|
if (!batch || batch.error) {
|
|
255
|
-
throw new
|
|
256
|
+
throw new CommandExecutionError(batch?.error || 'LinkedIn search returned an unexpected response');
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
const elements: any[] = Array.isArray(batch?.elements) ? batch.elements : [];
|
|
@@ -387,7 +388,7 @@ cli({
|
|
|
387
388
|
const location = (kwargs.location ?? '').trim();
|
|
388
389
|
const keywords = String(kwargs.query ?? '').trim();
|
|
389
390
|
|
|
390
|
-
if (!keywords) throw new
|
|
391
|
+
if (!keywords) throw new ArgumentError('query is required');
|
|
391
392
|
|
|
392
393
|
const searchParams = new URLSearchParams({ keywords });
|
|
393
394
|
if (location) searchParams.set('location', location);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './timeline.js';
|
|
4
|
+
|
|
5
|
+
const { parseMetric, buildPostId, mergeTimelinePosts } = await import('./timeline.js').then(
|
|
6
|
+
(m) => (m as any).__test__,
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
describe('linkedin timeline adapter', () => {
|
|
10
|
+
const command = getRegistry().get('linkedin/timeline');
|
|
11
|
+
|
|
12
|
+
it('registers the command with correct shape', () => {
|
|
13
|
+
expect(command).toBeDefined();
|
|
14
|
+
expect(command!.site).toBe('linkedin');
|
|
15
|
+
expect(command!.name).toBe('timeline');
|
|
16
|
+
expect(command!.domain).toBe('www.linkedin.com');
|
|
17
|
+
expect(command!.strategy).toBe('cookie');
|
|
18
|
+
expect(command!.browser).toBe(true);
|
|
19
|
+
expect(typeof command!.func).toBe('function');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('has limit arg with default 20', () => {
|
|
23
|
+
const limitArg = command!.args.find((a) => a.name === 'limit');
|
|
24
|
+
expect(limitArg).toBeDefined();
|
|
25
|
+
expect(limitArg!.default).toBe(20);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('includes expected columns', () => {
|
|
29
|
+
expect(command!.columns).toEqual(
|
|
30
|
+
expect.arrayContaining(['author', 'text', 'reactions', 'comments', 'url']),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('parseMetric', () => {
|
|
36
|
+
it('parses plain numbers', () => {
|
|
37
|
+
expect(parseMetric('42')).toBe(42);
|
|
38
|
+
expect(parseMetric('1,234')).toBe(1234);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('handles k/m suffixes', () => {
|
|
42
|
+
expect(parseMetric('2.5k')).toBe(2500);
|
|
43
|
+
expect(parseMetric('1.2M')).toBe(1200000);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns 0 for empty/undefined', () => {
|
|
47
|
+
expect(parseMetric('')).toBe(0);
|
|
48
|
+
expect(parseMetric(undefined)).toBe(0);
|
|
49
|
+
expect(parseMetric(null)).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('buildPostId', () => {
|
|
54
|
+
it('uses url when present', () => {
|
|
55
|
+
expect(buildPostId({ url: 'https://linkedin.com/post/123' })).toBe(
|
|
56
|
+
'https://linkedin.com/post/123',
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('falls back to composite key', () => {
|
|
61
|
+
const id = buildPostId({ author: 'Alice', posted_at: '2h', text: 'Hello world' });
|
|
62
|
+
expect(id).toBe('Alice::2h::Hello world');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('mergeTimelinePosts', () => {
|
|
67
|
+
it('deduplicates by url', () => {
|
|
68
|
+
const url = 'https://linkedin.com/post/1';
|
|
69
|
+
const a = {
|
|
70
|
+
id: url,
|
|
71
|
+
author: 'Alice',
|
|
72
|
+
author_url: '',
|
|
73
|
+
headline: '',
|
|
74
|
+
text: 'Hello',
|
|
75
|
+
posted_at: '1h',
|
|
76
|
+
reactions: 5,
|
|
77
|
+
comments: 1,
|
|
78
|
+
url,
|
|
79
|
+
};
|
|
80
|
+
const result = mergeTimelinePosts([a], [a]);
|
|
81
|
+
expect(result).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('skips posts without author or text', () => {
|
|
85
|
+
const empty = {
|
|
86
|
+
id: '2',
|
|
87
|
+
author: '',
|
|
88
|
+
author_url: '',
|
|
89
|
+
headline: '',
|
|
90
|
+
text: 'some text',
|
|
91
|
+
posted_at: '',
|
|
92
|
+
reactions: 0,
|
|
93
|
+
comments: 0,
|
|
94
|
+
url: '',
|
|
95
|
+
};
|
|
96
|
+
const result = mergeTimelinePosts([], [empty]);
|
|
97
|
+
expect(result).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
});
|