@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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixiv download — download all images from an illustration.
|
|
3
|
+
*
|
|
4
|
+
* Pixiv's CDN (i.pximg.net) requires Referer: https://www.pixiv.net/ header.
|
|
5
|
+
* Uses the /ajax/illust/{id}/pages API to get original-quality image URLs.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { cli, Strategy } from '../../registry.js';
|
|
10
|
+
import { formatCookieHeader, httpDownload } from '../../download/index.js';
|
|
11
|
+
import { formatBytes } from '../../download/progress.js';
|
|
12
|
+
import { CommandExecutionError, getErrorMessage } from '../../errors.js';
|
|
13
|
+
import { pixivFetch } from './utils.js';
|
|
14
|
+
cli({
|
|
15
|
+
site: 'pixiv',
|
|
16
|
+
name: 'download',
|
|
17
|
+
description: 'Download illustration images from Pixiv',
|
|
18
|
+
domain: 'www.pixiv.net',
|
|
19
|
+
strategy: Strategy.COOKIE,
|
|
20
|
+
args: [
|
|
21
|
+
{ name: 'illust-id', positional: true, required: true, help: 'Illustration ID' },
|
|
22
|
+
{ name: 'output', default: './pixiv-downloads', help: 'Output directory' },
|
|
23
|
+
],
|
|
24
|
+
columns: ['index', 'type', 'status', 'size'],
|
|
25
|
+
func: async (page, kwargs) => {
|
|
26
|
+
const illustId = String(kwargs['illust-id'] ?? '');
|
|
27
|
+
const output = String(kwargs.output ?? './pixiv-downloads');
|
|
28
|
+
if (!/^\d+$/.test(illustId)) {
|
|
29
|
+
throw new CommandExecutionError(`Invalid illustration ID: ${illustId}`);
|
|
30
|
+
}
|
|
31
|
+
// pixivFetch handles navigate + error checking; returns the response body directly
|
|
32
|
+
const pages = await pixivFetch(page, `/ajax/illust/${illustId}/pages`, {
|
|
33
|
+
notFoundMsg: `Illustration not found: ${illustId}`,
|
|
34
|
+
}) || [];
|
|
35
|
+
if (pages.length === 0) {
|
|
36
|
+
return [{ index: 0, type: '-', status: 'failed', size: 'No images found' }];
|
|
37
|
+
}
|
|
38
|
+
// Extract cookies for authenticated downloads
|
|
39
|
+
const cookies = formatCookieHeader(await page.getCookies({ domain: 'pixiv.net' }));
|
|
40
|
+
// Create output directory
|
|
41
|
+
const outputDir = path.join(output, illustId);
|
|
42
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
43
|
+
const results = [];
|
|
44
|
+
for (let i = 0; i < pages.length; i++) {
|
|
45
|
+
const p = pages[i];
|
|
46
|
+
const url = p.urls?.original || p.urls?.regular || '';
|
|
47
|
+
if (!url) {
|
|
48
|
+
results.push({ index: i + 1, type: 'image', status: 'failed', size: 'No URL' });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const ext = path.extname(new URL(url).pathname) || '.jpg';
|
|
53
|
+
const filename = `${illustId}_p${i}${ext}`;
|
|
54
|
+
const destPath = path.join(outputDir, filename);
|
|
55
|
+
const result = await httpDownload(url, destPath, {
|
|
56
|
+
cookies,
|
|
57
|
+
headers: { Referer: 'https://www.pixiv.net/' },
|
|
58
|
+
timeout: 60000,
|
|
59
|
+
});
|
|
60
|
+
results.push({
|
|
61
|
+
index: i + 1,
|
|
62
|
+
type: 'image',
|
|
63
|
+
status: result.success ? 'success' : 'failed',
|
|
64
|
+
size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
results.push({
|
|
69
|
+
index: i + 1,
|
|
70
|
+
type: 'image',
|
|
71
|
+
status: 'failed',
|
|
72
|
+
size: getErrorMessage(err),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return results;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
|
|
4
|
+
import { createPageMock } from './test-utils.js';
|
|
5
|
+
// Mock download dependencies before importing the adapter
|
|
6
|
+
const { mockHttpDownload, mockMkdirSync } = vi.hoisted(() => ({
|
|
7
|
+
mockHttpDownload: vi.fn(),
|
|
8
|
+
mockMkdirSync: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('../../download/index.js', () => ({
|
|
11
|
+
formatCookieHeader: vi.fn().mockReturnValue('cookie=value'),
|
|
12
|
+
httpDownload: mockHttpDownload,
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('node:fs', () => ({
|
|
15
|
+
mkdirSync: mockMkdirSync,
|
|
16
|
+
}));
|
|
17
|
+
// Now import the adapter (after mocks are set up)
|
|
18
|
+
await import('./download.js');
|
|
19
|
+
let cmd;
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
cmd = getRegistry().get('pixiv/download');
|
|
22
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
23
|
+
});
|
|
24
|
+
describe('pixiv download', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mockHttpDownload.mockReset();
|
|
27
|
+
mockMkdirSync.mockReset();
|
|
28
|
+
});
|
|
29
|
+
it('throws CommandExecutionError on invalid illust ID', async () => {
|
|
30
|
+
const page = createPageMock([]);
|
|
31
|
+
await expect(cmd.func(page, { 'illust-id': 'abc', output: '/tmp/test' })).rejects.toThrow(CommandExecutionError);
|
|
32
|
+
});
|
|
33
|
+
it('throws AuthRequiredError on 403', async () => {
|
|
34
|
+
const page = createPageMock([{ __httpError: 403 }]);
|
|
35
|
+
await expect(cmd.func(page, { 'illust-id': '12345', output: '/tmp/test' })).rejects.toThrow(AuthRequiredError);
|
|
36
|
+
});
|
|
37
|
+
it('throws CommandExecutionError on 404', async () => {
|
|
38
|
+
const page = createPageMock([{ __httpError: 404 }]);
|
|
39
|
+
await expect(cmd.func(page, { 'illust-id': '12345', output: '/tmp/test' })).rejects.toThrow(CommandExecutionError);
|
|
40
|
+
});
|
|
41
|
+
it('throws CommandExecutionError on non-auth HTTP failure', async () => {
|
|
42
|
+
const page = createPageMock([{ __httpError: 500 }]);
|
|
43
|
+
await expect(cmd.func(page, { 'illust-id': '12345', output: '/tmp/test' })).rejects.toThrow(CommandExecutionError);
|
|
44
|
+
});
|
|
45
|
+
it('returns failure when no images found', async () => {
|
|
46
|
+
const page = createPageMock([{ body: [] }]);
|
|
47
|
+
const result = (await cmd.func(page, { 'illust-id': '12345', output: '/tmp/test' }));
|
|
48
|
+
expect(result).toEqual([{ index: 0, type: '-', status: 'failed', size: 'No images found' }]);
|
|
49
|
+
});
|
|
50
|
+
it('downloads images with Referer header', async () => {
|
|
51
|
+
mockHttpDownload.mockResolvedValue({ success: true, size: 1024000 });
|
|
52
|
+
const page = createPageMock([
|
|
53
|
+
{
|
|
54
|
+
body: [
|
|
55
|
+
{ urls: { original: 'https://i.pximg.net/img-original/img/2025/01/01/00/00/00/12345_p0.png' } },
|
|
56
|
+
{ urls: { original: 'https://i.pximg.net/img-original/img/2025/01/01/00/00/00/12345_p1.jpg' } },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
const result = (await cmd.func(page, { 'illust-id': '12345', output: '/tmp/test' }));
|
|
61
|
+
expect(result).toHaveLength(2);
|
|
62
|
+
expect(result[0]).toMatchObject({ index: 1, type: 'image', status: 'success' });
|
|
63
|
+
expect(result[1]).toMatchObject({ index: 2, type: 'image', status: 'success' });
|
|
64
|
+
// Verify Referer header was passed
|
|
65
|
+
expect(mockHttpDownload).toHaveBeenCalledTimes(2);
|
|
66
|
+
const firstCallOpts = mockHttpDownload.mock.calls[0][2];
|
|
67
|
+
expect(firstCallOpts.headers).toEqual({ Referer: 'https://www.pixiv.net/' });
|
|
68
|
+
});
|
|
69
|
+
it('handles individual download failures gracefully', async () => {
|
|
70
|
+
mockHttpDownload
|
|
71
|
+
.mockResolvedValueOnce({ success: true, size: 512000 })
|
|
72
|
+
.mockRejectedValueOnce(new Error('Connection timeout'));
|
|
73
|
+
const page = createPageMock([
|
|
74
|
+
{
|
|
75
|
+
body: [
|
|
76
|
+
{ urls: { original: 'https://i.pximg.net/img/12345_p0.png' } },
|
|
77
|
+
{ urls: { original: 'https://i.pximg.net/img/12345_p1.png' } },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
const result = (await cmd.func(page, { 'illust-id': '12345', output: '/tmp/test' }));
|
|
82
|
+
expect(result).toHaveLength(2);
|
|
83
|
+
expect(result[0].status).toBe('success');
|
|
84
|
+
expect(result[1].status).toBe('failed');
|
|
85
|
+
expect(result[1].size).toBe('Connection timeout');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixiv illusts — list illustrations by an artist.
|
|
3
|
+
*
|
|
4
|
+
* Two-step process:
|
|
5
|
+
* 1. Fetch all illust IDs from the user's profile
|
|
6
|
+
* 2. Batch-fetch details for the most recent ones (max 48 IDs per request)
|
|
7
|
+
*/
|
|
8
|
+
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
10
|
+
import { pixivFetch, BATCH_SIZE } from './utils.js';
|
|
11
|
+
cli({
|
|
12
|
+
site: 'pixiv',
|
|
13
|
+
name: 'illusts',
|
|
14
|
+
description: "List a Pixiv artist's illustrations",
|
|
15
|
+
domain: 'www.pixiv.net',
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'user-id', positional: true, required: true, help: 'Pixiv user ID' },
|
|
19
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['rank', 'title', 'illust_id', 'pages', 'bookmarks', 'tags', 'created'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const userId = String(kwargs['user-id'] ?? '');
|
|
24
|
+
const limit = Number(kwargs.limit) || 20;
|
|
25
|
+
if (!/^\d+$/.test(userId)) {
|
|
26
|
+
throw new CommandExecutionError(`Invalid user ID: ${userId}`);
|
|
27
|
+
}
|
|
28
|
+
// Step 1: get all illust IDs
|
|
29
|
+
const profileBody = await pixivFetch(page, `/ajax/user/${userId}/profile/all`, {
|
|
30
|
+
notFoundMsg: `User not found: ${userId}`,
|
|
31
|
+
});
|
|
32
|
+
const allIds = Object.keys(profileBody?.illusts || {})
|
|
33
|
+
.sort((a, b) => Number(b) - Number(a))
|
|
34
|
+
.slice(0, limit);
|
|
35
|
+
if (allIds.length === 0)
|
|
36
|
+
return [];
|
|
37
|
+
// Step 2: batch fetch details (Pixiv supports up to ~48 IDs per request)
|
|
38
|
+
const allWorks = {};
|
|
39
|
+
for (let offset = 0; offset < allIds.length; offset += BATCH_SIZE) {
|
|
40
|
+
const batch = allIds.slice(offset, offset + BATCH_SIZE);
|
|
41
|
+
const idsParam = batch.map(id => `ids[]=${id}`).join('&');
|
|
42
|
+
// pixivFetch navigates on each call; for subsequent batches we re-navigate,
|
|
43
|
+
// which is fine — the cookie is already attached.
|
|
44
|
+
const detailBody = await pixivFetch(page, `/ajax/user/${userId}/profile/illusts?${idsParam}&work_category=illustManga&is_first_page=${offset === 0 ? 1 : 0}`);
|
|
45
|
+
Object.assign(allWorks, detailBody?.works || {});
|
|
46
|
+
}
|
|
47
|
+
return allIds
|
|
48
|
+
.map((id, i) => {
|
|
49
|
+
const w = allWorks[id];
|
|
50
|
+
if (!w)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
rank: i + 1,
|
|
54
|
+
title: w.title || '',
|
|
55
|
+
illust_id: w.id,
|
|
56
|
+
pages: w.pageCount || 1,
|
|
57
|
+
bookmarks: w.bookmarkCount || 0,
|
|
58
|
+
tags: (w.tags || []).slice(0, 5).join(', '),
|
|
59
|
+
created: (w.createDate || '').split('T')[0],
|
|
60
|
+
url: 'https://www.pixiv.net/artworks/' + w.id,
|
|
61
|
+
};
|
|
62
|
+
})
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './illusts.js';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
|
|
4
|
+
import { createPageMock } from './test-utils.js';
|
|
5
|
+
import './illusts.js';
|
|
6
|
+
let cmd;
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
cmd = getRegistry().get('pixiv/illusts');
|
|
9
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
10
|
+
});
|
|
11
|
+
describe('pixiv illusts', () => {
|
|
12
|
+
it('throws CommandExecutionError on invalid user ID', async () => {
|
|
13
|
+
const page = createPageMock([]);
|
|
14
|
+
await expect(cmd.func(page, { 'user-id': 'abc', limit: 5 })).rejects.toThrow(CommandExecutionError);
|
|
15
|
+
});
|
|
16
|
+
it('throws AuthRequiredError on 401', async () => {
|
|
17
|
+
const page = createPageMock([{ __httpError: 401 }]);
|
|
18
|
+
await expect(cmd.func(page, { 'user-id': '11', limit: 5 })).rejects.toThrow(AuthRequiredError);
|
|
19
|
+
});
|
|
20
|
+
it('throws generic error on non-auth HTTP failure', async () => {
|
|
21
|
+
const page = createPageMock([{ __httpError: 500 }]);
|
|
22
|
+
await expect(cmd.func(page, { 'user-id': '11', limit: 5 })).rejects.toThrow(CommandExecutionError);
|
|
23
|
+
});
|
|
24
|
+
it('returns empty array when user has no illusts', async () => {
|
|
25
|
+
const page = createPageMock([
|
|
26
|
+
{ body: { illusts: {} } },
|
|
27
|
+
]);
|
|
28
|
+
const result = await cmd.func(page, { 'user-id': '11', limit: 5 });
|
|
29
|
+
expect(result).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
it('fetches illust IDs then batch-fetches details', async () => {
|
|
32
|
+
const page = createPageMock([
|
|
33
|
+
// Step 1: profile/all returns illust IDs
|
|
34
|
+
{
|
|
35
|
+
body: {
|
|
36
|
+
illusts: { '99999': null, '88888': null, '77777': null },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
// Step 2: batch detail response
|
|
40
|
+
{
|
|
41
|
+
body: {
|
|
42
|
+
works: {
|
|
43
|
+
'99999': {
|
|
44
|
+
id: '99999',
|
|
45
|
+
title: 'Latest Work',
|
|
46
|
+
pageCount: 2,
|
|
47
|
+
bookmarkCount: 300,
|
|
48
|
+
tags: ['original', 'fantasy'],
|
|
49
|
+
createDate: '2025-01-15T12:00:00+09:00',
|
|
50
|
+
},
|
|
51
|
+
'88888': {
|
|
52
|
+
id: '88888',
|
|
53
|
+
title: 'Older Work',
|
|
54
|
+
pageCount: 1,
|
|
55
|
+
bookmarkCount: 150,
|
|
56
|
+
tags: ['landscape'],
|
|
57
|
+
createDate: '2024-12-01T10:00:00+09:00',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
const result = (await cmd.func(page, { 'user-id': '11', limit: 3 }));
|
|
64
|
+
// Should be sorted newest first (99999 > 88888 > 77777)
|
|
65
|
+
expect(result).toHaveLength(2); // 77777 has no detail data, filtered out
|
|
66
|
+
expect(result[0]).toMatchObject({
|
|
67
|
+
rank: 1,
|
|
68
|
+
title: 'Latest Work',
|
|
69
|
+
illust_id: '99999',
|
|
70
|
+
pages: 2,
|
|
71
|
+
bookmarks: 300,
|
|
72
|
+
created: '2025-01-15',
|
|
73
|
+
});
|
|
74
|
+
expect(result[1]).toMatchObject({
|
|
75
|
+
rank: 2,
|
|
76
|
+
title: 'Older Work',
|
|
77
|
+
illust_id: '88888',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it('respects the limit on illust IDs fetched', async () => {
|
|
81
|
+
const page = createPageMock([
|
|
82
|
+
{
|
|
83
|
+
body: {
|
|
84
|
+
illusts: { '100': null, '200': null, '300': null, '400': null, '500': null },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
body: {
|
|
89
|
+
works: {
|
|
90
|
+
'500': { id: '500', title: 'W5', pageCount: 1, bookmarkCount: 0, tags: [], createDate: '' },
|
|
91
|
+
'400': { id: '400', title: 'W4', pageCount: 1, bookmarkCount: 0, tags: [], createDate: '' },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
const result = (await cmd.func(page, { 'user-id': '11', limit: 2 }));
|
|
97
|
+
expect(result).toHaveLength(2);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
site: pixiv
|
|
2
|
+
name: ranking
|
|
3
|
+
description: Pixiv illustration rankings (daily/weekly/monthly)
|
|
4
|
+
domain: www.pixiv.net
|
|
5
|
+
strategy: cookie
|
|
6
|
+
browser: true
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
mode:
|
|
10
|
+
type: str
|
|
11
|
+
default: daily
|
|
12
|
+
description: Ranking mode
|
|
13
|
+
choices: [daily, weekly, monthly, rookie, original, male, female, daily_r18, weekly_r18]
|
|
14
|
+
page:
|
|
15
|
+
type: int
|
|
16
|
+
default: 1
|
|
17
|
+
description: Page number
|
|
18
|
+
limit:
|
|
19
|
+
type: int
|
|
20
|
+
default: 20
|
|
21
|
+
description: Number of results
|
|
22
|
+
|
|
23
|
+
pipeline:
|
|
24
|
+
- navigate: https://www.pixiv.net
|
|
25
|
+
|
|
26
|
+
- evaluate: |
|
|
27
|
+
(async () => {
|
|
28
|
+
const mode = ${{ args.mode | json }};
|
|
29
|
+
const page = ${{ args.page | json }};
|
|
30
|
+
const limit = ${{ args.limit | json }};
|
|
31
|
+
const res = await fetch(
|
|
32
|
+
'https://www.pixiv.net/ranking.php?mode=' + mode + '&p=' + page + '&format=json',
|
|
33
|
+
{ credentials: 'include' }
|
|
34
|
+
);
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
if (res.status === 401 || res.status === 403) throw new Error('Authentication required — please log in to Pixiv in Chrome');
|
|
37
|
+
throw new Error('Pixiv request failed (HTTP ' + res.status + ')');
|
|
38
|
+
}
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const items = (data?.contents || []).slice(0, limit);
|
|
41
|
+
return items.map((item, i) => ({
|
|
42
|
+
rank: item.rank,
|
|
43
|
+
title: item.title,
|
|
44
|
+
author: item.user_name,
|
|
45
|
+
user_id: item.user_id,
|
|
46
|
+
illust_id: item.illust_id,
|
|
47
|
+
pages: item.illust_page_count,
|
|
48
|
+
bookmarks: item.illust_bookmark_count,
|
|
49
|
+
url: 'https://www.pixiv.net/artworks/' + item.illust_id
|
|
50
|
+
}));
|
|
51
|
+
})()
|
|
52
|
+
|
|
53
|
+
columns: [rank, title, author, illust_id, pages, bookmarks]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixiv search — search illustrations by keyword/tag.
|
|
3
|
+
*
|
|
4
|
+
* Uses the internal Ajax search API with browser cookies for authentication.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { pixivFetch } from './utils.js';
|
|
8
|
+
cli({
|
|
9
|
+
site: 'pixiv',
|
|
10
|
+
name: 'search',
|
|
11
|
+
description: 'Search Pixiv illustrations by keyword',
|
|
12
|
+
domain: 'www.pixiv.net',
|
|
13
|
+
strategy: Strategy.COOKIE,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword or tag' },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
17
|
+
{ name: 'order', type: 'str', default: 'date_d', help: 'Sort order', choices: ['date_d', 'date', 'popular_d', 'popular_male_d', 'popular_female_d'] },
|
|
18
|
+
{ name: 'mode', type: 'str', default: 'all', help: 'Search mode', choices: ['all', 'safe', 'r18'] },
|
|
19
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['rank', 'title', 'author', 'illust_id', 'pages', 'bookmarks', 'tags'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const { query, limit = 20, order = 'date_d', mode = 'all', page: pageNum = 1 } = kwargs;
|
|
24
|
+
const encoded = encodeURIComponent(query);
|
|
25
|
+
// Pixiv search API requires the keyword in BOTH the URL path and the `word` query param.
|
|
26
|
+
const body = await pixivFetch(page, `/ajax/search/illustrations/${encoded}`, { params: { word: query, order, mode, p: pageNum, s_mode: 's_tag_full', type: 'illust_and_ugoira' } });
|
|
27
|
+
const items = body?.illust?.data || [];
|
|
28
|
+
return items
|
|
29
|
+
.filter((item) => item.id)
|
|
30
|
+
.slice(0, Number(limit))
|
|
31
|
+
.map((item, i) => ({
|
|
32
|
+
rank: i + 1,
|
|
33
|
+
title: item.title || '',
|
|
34
|
+
author: item.userName || '',
|
|
35
|
+
user_id: item.userId || '',
|
|
36
|
+
illust_id: item.id,
|
|
37
|
+
pages: item.pageCount || 1,
|
|
38
|
+
bookmarks: item.bookmarkCount || 0,
|
|
39
|
+
tags: (item.tags || []).slice(0, 5).join(', '),
|
|
40
|
+
url: 'https://www.pixiv.net/artworks/' + item.id,
|
|
41
|
+
}));
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './search.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
|
|
4
|
+
import { createPageMock } from './test-utils.js';
|
|
5
|
+
import './search.js';
|
|
6
|
+
let cmd;
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
cmd = getRegistry().get('pixiv/search');
|
|
9
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
10
|
+
});
|
|
11
|
+
describe('pixiv search', () => {
|
|
12
|
+
it('throws AuthRequiredError on 401', async () => {
|
|
13
|
+
const page = createPageMock([{ __httpError: 401 }]);
|
|
14
|
+
await expect(cmd.func(page, { query: '初音ミク', limit: 5 })).rejects.toThrow(AuthRequiredError);
|
|
15
|
+
});
|
|
16
|
+
it('throws generic error on non-auth HTTP failure', async () => {
|
|
17
|
+
const page = createPageMock([{ __httpError: 500 }]);
|
|
18
|
+
await expect(cmd.func(page, { query: 'test', limit: 5 })).rejects.toThrow(CommandExecutionError);
|
|
19
|
+
});
|
|
20
|
+
it('returns ranked results with correct fields', async () => {
|
|
21
|
+
const page = createPageMock([
|
|
22
|
+
{
|
|
23
|
+
body: {
|
|
24
|
+
illust: {
|
|
25
|
+
data: [
|
|
26
|
+
{
|
|
27
|
+
id: '12345',
|
|
28
|
+
title: 'Miku Illustration',
|
|
29
|
+
userName: 'artist1',
|
|
30
|
+
userId: '100',
|
|
31
|
+
pageCount: 3,
|
|
32
|
+
bookmarkCount: 500,
|
|
33
|
+
tags: ['初音ミク', 'VOCALOID', 'ミク'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: '67890',
|
|
37
|
+
title: 'Another Art',
|
|
38
|
+
userName: 'artist2',
|
|
39
|
+
userId: '200',
|
|
40
|
+
pageCount: 1,
|
|
41
|
+
bookmarkCount: 100,
|
|
42
|
+
tags: ['オリジナル'],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
const result = (await cmd.func(page, { query: '初音ミク', limit: 10 }));
|
|
50
|
+
expect(result).toHaveLength(2);
|
|
51
|
+
expect(result[0]).toMatchObject({
|
|
52
|
+
rank: 1,
|
|
53
|
+
title: 'Miku Illustration',
|
|
54
|
+
author: 'artist1',
|
|
55
|
+
illust_id: '12345',
|
|
56
|
+
pages: 3,
|
|
57
|
+
bookmarks: 500,
|
|
58
|
+
});
|
|
59
|
+
expect(result[1]).toMatchObject({ rank: 2, illust_id: '67890' });
|
|
60
|
+
});
|
|
61
|
+
it('respects the limit parameter', async () => {
|
|
62
|
+
const page = createPageMock([
|
|
63
|
+
{
|
|
64
|
+
body: {
|
|
65
|
+
illust: {
|
|
66
|
+
data: [
|
|
67
|
+
{ id: '1', title: 'A', userName: 'u1', userId: '1', pageCount: 1, bookmarkCount: 0, tags: [] },
|
|
68
|
+
{ id: '2', title: 'B', userName: 'u2', userId: '2', pageCount: 1, bookmarkCount: 0, tags: [] },
|
|
69
|
+
{ id: '3', title: 'C', userName: 'u3', userId: '3', pageCount: 1, bookmarkCount: 0, tags: [] },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const result = (await cmd.func(page, { query: 'test', limit: 2 }));
|
|
76
|
+
expect(result).toHaveLength(2);
|
|
77
|
+
});
|
|
78
|
+
it('returns empty array when no results', async () => {
|
|
79
|
+
const page = createPageMock([{ body: { illust: { data: [] } } }]);
|
|
80
|
+
const result = await cmd.func(page, { query: 'nonexistent', limit: 10 });
|
|
81
|
+
expect(result).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IPage } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a minimal page mock with only the methods commonly used by Pixiv adapters.
|
|
4
|
+
*
|
|
5
|
+
* Since all TS adapters now go through `pixivFetch` which calls `page.evaluate`,
|
|
6
|
+
* the evaluate results should match the raw Pixiv Ajax response format:
|
|
7
|
+
* - Success: `{ body: ... }` (pixivFetch returns the `body` field)
|
|
8
|
+
* - HTTP error: `{ __httpError: <status> }` (pixivFetch detects and throws)
|
|
9
|
+
*
|
|
10
|
+
* Additional methods can be overridden via the overrides parameter.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createPageMock(evaluateResults: any[], overrides?: Partial<IPage>): IPage;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
/**
|
|
3
|
+
* Create a minimal page mock with only the methods commonly used by Pixiv adapters.
|
|
4
|
+
*
|
|
5
|
+
* Since all TS adapters now go through `pixivFetch` which calls `page.evaluate`,
|
|
6
|
+
* the evaluate results should match the raw Pixiv Ajax response format:
|
|
7
|
+
* - Success: `{ body: ... }` (pixivFetch returns the `body` field)
|
|
8
|
+
* - HTTP error: `{ __httpError: <status> }` (pixivFetch detects and throws)
|
|
9
|
+
*
|
|
10
|
+
* Additional methods can be overridden via the overrides parameter.
|
|
11
|
+
*/
|
|
12
|
+
export function createPageMock(evaluateResults, overrides) {
|
|
13
|
+
const evaluate = vi.fn();
|
|
14
|
+
for (const result of evaluateResults) {
|
|
15
|
+
evaluate.mockResolvedValueOnce(result);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
evaluate,
|
|
20
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
site: pixiv
|
|
2
|
+
name: user
|
|
3
|
+
description: View Pixiv artist profile
|
|
4
|
+
domain: www.pixiv.net
|
|
5
|
+
strategy: cookie
|
|
6
|
+
browser: true
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
uid:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
positional: true
|
|
13
|
+
description: Pixiv user ID
|
|
14
|
+
|
|
15
|
+
pipeline:
|
|
16
|
+
- navigate: https://www.pixiv.net
|
|
17
|
+
|
|
18
|
+
- evaluate: |
|
|
19
|
+
(async () => {
|
|
20
|
+
const uid = ${{ args.uid | json }};
|
|
21
|
+
const res = await fetch(
|
|
22
|
+
'https://www.pixiv.net/ajax/user/' + uid + '?full=1',
|
|
23
|
+
{ credentials: 'include' }
|
|
24
|
+
);
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
if (res.status === 401 || res.status === 403) throw new Error('Authentication required — please log in to Pixiv in Chrome');
|
|
27
|
+
if (res.status === 404) throw new Error('User not found: ' + uid);
|
|
28
|
+
throw new Error('Pixiv request failed (HTTP ' + res.status + ')');
|
|
29
|
+
}
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
const b = data?.body;
|
|
32
|
+
if (!b) throw new Error('User not found');
|
|
33
|
+
return [{
|
|
34
|
+
user_id: uid,
|
|
35
|
+
name: b.name,
|
|
36
|
+
premium: b.premium ? 'Yes' : 'No',
|
|
37
|
+
following: b.following,
|
|
38
|
+
illusts: typeof b.illusts === 'object' ? Object.keys(b.illusts).length : (b.illusts || 0),
|
|
39
|
+
manga: typeof b.manga === 'object' ? Object.keys(b.manga).length : (b.manga || 0),
|
|
40
|
+
novels: typeof b.novels === 'object' ? Object.keys(b.novels).length : (b.novels || 0),
|
|
41
|
+
comment: (b.comment || '').slice(0, 80),
|
|
42
|
+
url: 'https://www.pixiv.net/users/' + uid
|
|
43
|
+
}];
|
|
44
|
+
})()
|
|
45
|
+
|
|
46
|
+
columns: [user_id, name, premium, following, illusts, manga, novels, comment]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixiv shared helpers: authenticated Ajax fetch with standard error handling.
|
|
3
|
+
*
|
|
4
|
+
* All Pixiv Ajax APIs return `{ error: false, body: ... }` on success.
|
|
5
|
+
* On failure the HTTP status code is used to distinguish auth (401/403),
|
|
6
|
+
* not-found (404), and other errors.
|
|
7
|
+
*/
|
|
8
|
+
import type { IPage } from '../../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Navigate to Pixiv (to attach cookies) then fetch a Pixiv Ajax API endpoint.
|
|
11
|
+
*
|
|
12
|
+
* Handles the common navigate → evaluate(fetch) → error-check pattern used
|
|
13
|
+
* by every Pixiv TS adapter.
|
|
14
|
+
*
|
|
15
|
+
* @param page - Browser page instance
|
|
16
|
+
* @param path - API path, e.g. '/ajax/illust/12345'
|
|
17
|
+
* @param opts - Optional query params
|
|
18
|
+
* @returns - The parsed `body` from the JSON response
|
|
19
|
+
* @throws AuthRequiredError on 401/403
|
|
20
|
+
* @throws CommandExecutionError on 404 or other HTTP errors
|
|
21
|
+
*/
|
|
22
|
+
export declare function pixivFetch(page: IPage, path: string, opts?: {
|
|
23
|
+
params?: Record<string, string | number>;
|
|
24
|
+
notFoundMsg?: string;
|
|
25
|
+
}): Promise<any>;
|
|
26
|
+
/** Maximum number of illust IDs per batch detail request (Pixiv server limit). */
|
|
27
|
+
export declare const BATCH_SIZE = 48;
|