@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for Danjuan (蛋卷基金) adapters.
|
|
3
|
+
*
|
|
4
|
+
* Core design: a single page.evaluate call fetches the gain overview AND
|
|
5
|
+
* all per-account holdings in parallel (Promise.all), minimising Node↔Browser
|
|
6
|
+
* round-trips to exactly one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IPage } from '../../types.js';
|
|
10
|
+
|
|
11
|
+
export const DANJUAN_DOMAIN = 'danjuanfunds.com';
|
|
12
|
+
export const DANJUAN_ASSET_PAGE = `https://${DANJUAN_DOMAIN}/my-money`;
|
|
13
|
+
|
|
14
|
+
const GAIN_URL = `https://${DANJUAN_DOMAIN}/djapi/fundx/profit/assets/gain?gains=%5B%22private%22%5D`;
|
|
15
|
+
const SUMMARY_URL = `https://${DANJUAN_DOMAIN}/djapi/fundx/profit/assets/summary?invest_account_id=`;
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Types — keep everything explicit so TS consumers get autocomplete.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface DanjuanAccount {
|
|
22
|
+
accountId: string;
|
|
23
|
+
accountName: string;
|
|
24
|
+
accountType: string;
|
|
25
|
+
accountCode: string;
|
|
26
|
+
marketValue: number | null;
|
|
27
|
+
dailyGain: number | null;
|
|
28
|
+
mainFlag: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DanjuanHolding {
|
|
32
|
+
accountId: string;
|
|
33
|
+
accountName: string;
|
|
34
|
+
accountType: string;
|
|
35
|
+
fdCode: string;
|
|
36
|
+
fdName: string;
|
|
37
|
+
category: string;
|
|
38
|
+
marketValue: number | null;
|
|
39
|
+
volume: number | null;
|
|
40
|
+
usableRemainShare: number | null;
|
|
41
|
+
dailyGain: number | null;
|
|
42
|
+
holdGain: number | null;
|
|
43
|
+
holdGainRate: number | null;
|
|
44
|
+
totalGain: number | null;
|
|
45
|
+
nav: number | null;
|
|
46
|
+
marketPercent: number | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DanjuanSnapshot {
|
|
50
|
+
asOf: string | null;
|
|
51
|
+
totalAssetAmount: number | null;
|
|
52
|
+
totalAssetDailyGain: number | null;
|
|
53
|
+
totalAssetHoldGain: number | null;
|
|
54
|
+
totalAssetTotalGain: number | null;
|
|
55
|
+
totalFundMarketValue: number | null;
|
|
56
|
+
accounts: DanjuanAccount[];
|
|
57
|
+
holdings: DanjuanHolding[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Single-evaluate fetcher
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Fetch the complete Danjuan fund picture in ONE browser round-trip.
|
|
66
|
+
*
|
|
67
|
+
* Inside the browser context we:
|
|
68
|
+
* 1. Fetch the gain/assets overview (contains account list)
|
|
69
|
+
* 2. Promise.all → fetch every account's holdings in parallel
|
|
70
|
+
* 3. Return the combined result to Node
|
|
71
|
+
*/
|
|
72
|
+
export async function fetchDanjuanAll(page: IPage): Promise<DanjuanSnapshot> {
|
|
73
|
+
const raw: any = await page.evaluate(`
|
|
74
|
+
(async () => {
|
|
75
|
+
const f = async (u) => {
|
|
76
|
+
const r = await fetch(u, { credentials: 'include' });
|
|
77
|
+
if (!r.ok) return { _err: r.status };
|
|
78
|
+
try { return await r.json(); } catch { return { _err: 'parse' }; }
|
|
79
|
+
};
|
|
80
|
+
const n = (v) => { const x = Number(v); return Number.isFinite(x) ? x : null; };
|
|
81
|
+
|
|
82
|
+
const gain = await f(${JSON.stringify(GAIN_URL)});
|
|
83
|
+
if (gain._err) return { _httpError: gain._err };
|
|
84
|
+
|
|
85
|
+
const root = gain.data || {};
|
|
86
|
+
const fundSec = (root.items || []).find(i => i && i.summary_type === 'FUND');
|
|
87
|
+
const rawAccs = fundSec && Array.isArray(fundSec.invest_account_list)
|
|
88
|
+
? fundSec.invest_account_list : [];
|
|
89
|
+
|
|
90
|
+
const accounts = rawAccs.map(a => ({
|
|
91
|
+
accountId: String(a.invest_account_id || ''),
|
|
92
|
+
accountName: a.invest_account_name || '',
|
|
93
|
+
accountType: a.invest_account_type || '',
|
|
94
|
+
accountCode: a.invest_account_code || '',
|
|
95
|
+
marketValue: n(a.market_value),
|
|
96
|
+
dailyGain: n(a.daily_gain),
|
|
97
|
+
mainFlag: !!a.main_flag,
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
if (!accounts.length) {
|
|
101
|
+
return { _emptyAccounts: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const details = await Promise.all(
|
|
105
|
+
accounts.map(a => f(${JSON.stringify(SUMMARY_URL)} + encodeURIComponent(a.accountId)))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const holdings = [];
|
|
109
|
+
const detailErrors = [];
|
|
110
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
111
|
+
const d = details[i];
|
|
112
|
+
if (d._err) {
|
|
113
|
+
detailErrors.push({
|
|
114
|
+
accountId: accounts[i].accountId,
|
|
115
|
+
accountName: accounts[i].accountName,
|
|
116
|
+
error: d._err,
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const data = d.data || {};
|
|
121
|
+
const funds = Array.isArray(data.items) ? data.items : [];
|
|
122
|
+
const acc = accounts[i];
|
|
123
|
+
for (const fd of funds) {
|
|
124
|
+
holdings.push({
|
|
125
|
+
accountId: acc.accountId,
|
|
126
|
+
accountName: data.invest_account_name || acc.accountName,
|
|
127
|
+
accountType: data.invest_account_type || acc.accountType,
|
|
128
|
+
fdCode: fd.fd_code || '',
|
|
129
|
+
fdName: fd.fd_name || '',
|
|
130
|
+
category: fd.category_text || fd.category || '',
|
|
131
|
+
marketValue: n(fd.market_value),
|
|
132
|
+
volume: n(fd.volume),
|
|
133
|
+
usableRemainShare:n(fd.usable_remain_share),
|
|
134
|
+
dailyGain: n(fd.daily_gain),
|
|
135
|
+
holdGain: n(fd.hold_gain),
|
|
136
|
+
holdGainRate: n(fd.hold_gain_rate),
|
|
137
|
+
totalGain: n(fd.total_gain),
|
|
138
|
+
nav: n(fd.nav),
|
|
139
|
+
marketPercent: n(fd.market_percent),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
asOf: root.daily_gain_date || null,
|
|
146
|
+
totalAssetAmount: n(root.amount),
|
|
147
|
+
totalAssetDailyGain: n(root.daily_gain),
|
|
148
|
+
totalAssetHoldGain: n(root.hold_gain),
|
|
149
|
+
totalAssetTotalGain: n(root.total_gain),
|
|
150
|
+
totalFundMarketValue:n(fundSec && fundSec.amount),
|
|
151
|
+
accounts,
|
|
152
|
+
holdings,
|
|
153
|
+
detailErrors,
|
|
154
|
+
};
|
|
155
|
+
})()
|
|
156
|
+
`);
|
|
157
|
+
|
|
158
|
+
if (raw?._httpError) {
|
|
159
|
+
throw new Error(`HTTP ${raw._httpError} — Hint: not logged in to ${DANJUAN_DOMAIN}?`);
|
|
160
|
+
}
|
|
161
|
+
if (raw?._emptyAccounts) {
|
|
162
|
+
throw new Error(`No fund accounts found — Hint: not logged in to ${DANJUAN_DOMAIN}?`);
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(raw?.detailErrors) && raw.detailErrors.length > 0) {
|
|
165
|
+
const failedAccounts = raw.detailErrors
|
|
166
|
+
.map((item: { accountName?: string; accountId?: string; error?: string | number }) => {
|
|
167
|
+
const label = item.accountName && item.accountId
|
|
168
|
+
? `${item.accountName} (${item.accountId})`
|
|
169
|
+
: item.accountName || item.accountId || 'unknown account';
|
|
170
|
+
return `${label}: ${item.error}`;
|
|
171
|
+
})
|
|
172
|
+
.join(', ');
|
|
173
|
+
throw new Error(`Failed to fetch Danjuan account details: ${failedAccounts}`);
|
|
174
|
+
}
|
|
175
|
+
return raw as DanjuanSnapshot;
|
|
176
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
import { fetchDanjuanAll } from './danjuan-utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'xueqiu',
|
|
7
|
+
name: 'fund-holdings',
|
|
8
|
+
description: '获取蛋卷基金持仓明细(可用 --account 按子账户过滤)',
|
|
9
|
+
domain: 'danjuanfunds.com',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
navigateBefore: 'https://danjuanfunds.com/my-money',
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'account', type: 'str', default: '', help: '按子账户名称或 ID 过滤' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['accountName', 'fdCode', 'fdName', 'marketValue', 'volume', 'dailyGain', 'holdGain', 'holdGainRate', 'marketPercent'],
|
|
16
|
+
func: async (page: IPage, args) => {
|
|
17
|
+
const snapshot = await fetchDanjuanAll(page);
|
|
18
|
+
if (!snapshot.accounts.length) {
|
|
19
|
+
throw new Error('No fund accounts found — Hint: not logged in to danjuanfunds.com?');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const filter = String(args.account ?? '').trim();
|
|
23
|
+
const rows = filter
|
|
24
|
+
? snapshot.holdings.filter(h => h.accountId === filter || h.accountName.includes(filter))
|
|
25
|
+
: snapshot.holdings;
|
|
26
|
+
|
|
27
|
+
if (!rows.length) {
|
|
28
|
+
throw new Error(filter ? `No holdings matched account filter: ${filter}` : 'No holdings found.');
|
|
29
|
+
}
|
|
30
|
+
return rows;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
import { fetchDanjuanAll } from './danjuan-utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'xueqiu',
|
|
7
|
+
name: 'fund-snapshot',
|
|
8
|
+
description: '获取蛋卷基金快照(总资产、子账户、持仓,推荐 -f json 输出)',
|
|
9
|
+
domain: 'danjuanfunds.com',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
navigateBefore: 'https://danjuanfunds.com/my-money',
|
|
12
|
+
args: [],
|
|
13
|
+
columns: ['asOf', 'totalAssetAmount', 'totalFundMarketValue', 'accountCount', 'holdingCount'],
|
|
14
|
+
func: async (page: IPage) => {
|
|
15
|
+
const s = await fetchDanjuanAll(page);
|
|
16
|
+
return [{
|
|
17
|
+
asOf: s.asOf,
|
|
18
|
+
totalAssetAmount: s.totalAssetAmount,
|
|
19
|
+
totalAssetDailyGain: s.totalAssetDailyGain,
|
|
20
|
+
totalFundMarketValue: s.totalFundMarketValue,
|
|
21
|
+
accountCount: s.accounts.length,
|
|
22
|
+
holdingCount: s.holdings.length,
|
|
23
|
+
accounts: s.accounts,
|
|
24
|
+
holdings: s.holdings,
|
|
25
|
+
}];
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type RawSegment,
|
|
18
18
|
type Chapter,
|
|
19
19
|
} from './transcript-group.js';
|
|
20
|
+
import { CommandExecutionError, EmptyResultError } from '../../errors.js';
|
|
20
21
|
|
|
21
22
|
cli({
|
|
22
23
|
site: 'youtube',
|
|
@@ -91,10 +92,10 @@ cli({
|
|
|
91
92
|
`);
|
|
92
93
|
|
|
93
94
|
if (!captionData || typeof captionData === 'string') {
|
|
94
|
-
throw new
|
|
95
|
+
throw new CommandExecutionError(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'null response'}`);
|
|
95
96
|
}
|
|
96
97
|
if (captionData.error) {
|
|
97
|
-
throw new
|
|
98
|
+
throw new CommandExecutionError(`${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
// Warn if --lang was specified but not matched
|
|
@@ -176,10 +177,10 @@ cli({
|
|
|
176
177
|
`);
|
|
177
178
|
|
|
178
179
|
if (!Array.isArray(segments)) {
|
|
179
|
-
throw new
|
|
180
|
+
throw new CommandExecutionError((segments as any)?.error || 'Failed to parse caption segments');
|
|
180
181
|
}
|
|
181
182
|
if (segments.length === 0) {
|
|
182
|
-
throw new
|
|
183
|
+
throw new EmptyResultError('youtube transcript');
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
// Step 3: Fetch chapters (for grouped mode)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '../../registry.js';
|
|
5
5
|
import { parseVideoId } from './utils.js';
|
|
6
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
6
7
|
|
|
7
8
|
cli({
|
|
8
9
|
site: 'youtube',
|
|
@@ -104,8 +105,8 @@ cli({
|
|
|
104
105
|
})()
|
|
105
106
|
`);
|
|
106
107
|
|
|
107
|
-
if (!data || typeof data !== 'object') throw new
|
|
108
|
-
if (data.error) throw new
|
|
108
|
+
if (!data || typeof data !== 'object') throw new CommandExecutionError('Failed to extract video metadata from page');
|
|
109
|
+
if (data.error) throw new CommandExecutionError(data.error);
|
|
109
110
|
|
|
110
111
|
// Return as field/value pairs for table display
|
|
111
112
|
return Object.entries(data).map(([field, value]) => ({
|
package/src/daemon.ts
CHANGED
|
@@ -64,13 +64,14 @@ function readBody(req: IncomingMessage): Promise<string> {
|
|
|
64
64
|
return new Promise((resolve, reject) => {
|
|
65
65
|
const chunks: Buffer[] = [];
|
|
66
66
|
let size = 0;
|
|
67
|
+
let aborted = false;
|
|
67
68
|
req.on('data', (c: Buffer) => {
|
|
68
69
|
size += c.length;
|
|
69
|
-
if (size > MAX_BODY) { req.destroy(); reject(new Error('Body too large')); return; }
|
|
70
|
+
if (size > MAX_BODY) { aborted = true; req.destroy(); reject(new Error('Body too large')); return; }
|
|
70
71
|
chunks.push(c);
|
|
71
72
|
});
|
|
72
|
-
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
73
|
-
req.on('error', reject);
|
|
73
|
+
req.on('end', () => { if (!aborted) resolve(Buffer.concat(chunks).toString('utf-8')); });
|
|
74
|
+
req.on('error', (err) => { if (!aborted) reject(err); });
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -271,7 +272,7 @@ httpServer.listen(PORT, '127.0.0.1', () => {
|
|
|
271
272
|
httpServer.on('error', (err: NodeJS.ErrnoException) => {
|
|
272
273
|
if (err.code === 'EADDRINUSE') {
|
|
273
274
|
console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
|
|
274
|
-
process.exit(
|
|
275
|
+
process.exit(1);
|
|
275
276
|
}
|
|
276
277
|
console.error('[daemon] Server error:', err.message);
|
|
277
278
|
process.exit(1);
|
package/src/discovery.ts
CHANGED
|
@@ -20,31 +20,10 @@ import type { ManifestEntry } from './build-manifest.js';
|
|
|
20
20
|
|
|
21
21
|
/** Plugins directory: ~/.opencli/plugins/ */
|
|
22
22
|
export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
|
|
23
|
-
|
|
23
|
+
/** Matches files that register commands via cli() or lifecycle hooks */
|
|
24
|
+
const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
type?: string;
|
|
27
|
-
default?: unknown;
|
|
28
|
-
required?: boolean;
|
|
29
|
-
positional?: boolean;
|
|
30
|
-
description?: string;
|
|
31
|
-
help?: string;
|
|
32
|
-
choices?: string[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface YamlCliDefinition {
|
|
36
|
-
site?: string;
|
|
37
|
-
name?: string;
|
|
38
|
-
description?: string;
|
|
39
|
-
domain?: string;
|
|
40
|
-
strategy?: string;
|
|
41
|
-
browser?: boolean;
|
|
42
|
-
args?: Record<string, YamlArgDefinition>;
|
|
43
|
-
columns?: string[];
|
|
44
|
-
pipeline?: Record<string, unknown>[];
|
|
45
|
-
timeout?: number;
|
|
46
|
-
navigateBefore?: boolean | string;
|
|
47
|
-
}
|
|
26
|
+
import type { YamlCliDefinition } from './yaml-schema.js';
|
|
48
27
|
|
|
49
28
|
function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
|
|
50
29
|
if (!rawStrategy) return fallback;
|
|
@@ -52,9 +31,7 @@ function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Str
|
|
|
52
31
|
return Strategy[key] ?? fallback;
|
|
53
32
|
}
|
|
54
33
|
|
|
55
|
-
|
|
56
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
57
|
-
}
|
|
34
|
+
import { isRecord } from './utils.js';
|
|
58
35
|
|
|
59
36
|
/**
|
|
60
37
|
* Discover and register CLI commands.
|
|
@@ -66,12 +43,12 @@ export async function discoverClis(...dirs: string[]): Promise<void> {
|
|
|
66
43
|
const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
|
|
67
44
|
try {
|
|
68
45
|
await fs.promises.access(manifestPath);
|
|
69
|
-
await loadFromManifest(manifestPath, dir);
|
|
70
|
-
continue; // Skip filesystem scan
|
|
46
|
+
const loaded = await loadFromManifest(manifestPath, dir);
|
|
47
|
+
if (loaded) continue; // Skip filesystem scan only when manifest is usable
|
|
71
48
|
} catch {
|
|
72
|
-
//
|
|
73
|
-
await discoverClisFromFs(dir);
|
|
49
|
+
// Fall through to filesystem scan
|
|
74
50
|
}
|
|
51
|
+
await discoverClisFromFs(dir);
|
|
75
52
|
}
|
|
76
53
|
}
|
|
77
54
|
|
|
@@ -80,7 +57,7 @@ export async function discoverClis(...dirs: string[]): Promise<void> {
|
|
|
80
57
|
* YAML pipelines are inlined — zero YAML parsing at runtime.
|
|
81
58
|
* TS modules are deferred — loaded lazily on first execution.
|
|
82
59
|
*/
|
|
83
|
-
async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
60
|
+
async function loadFromManifest(manifestPath: string, clisDir: string): Promise<boolean> {
|
|
84
61
|
try {
|
|
85
62
|
const raw = await fs.promises.readFile(manifestPath, 'utf-8');
|
|
86
63
|
const manifest = JSON.parse(raw) as ManifestEntry[];
|
|
@@ -126,8 +103,10 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
126
103
|
registerCommand(cmd);
|
|
127
104
|
}
|
|
128
105
|
}
|
|
106
|
+
return true;
|
|
129
107
|
} catch (err) {
|
|
130
108
|
log.warn(`Failed to load manifest ${manifestPath}: ${getErrorMessage(err)}`);
|
|
109
|
+
return false;
|
|
131
110
|
}
|
|
132
111
|
}
|
|
133
112
|
|
|
@@ -136,7 +115,6 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
136
115
|
*/
|
|
137
116
|
async function discoverClisFromFs(dir: string): Promise<void> {
|
|
138
117
|
try { await fs.promises.access(dir); } catch { return; }
|
|
139
|
-
const promises: Promise<unknown>[] = [];
|
|
140
118
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
141
119
|
|
|
142
120
|
const sitePromises = entries
|
|
@@ -269,7 +247,7 @@ async function discoverPluginDir(dir: string, site: string): Promise<void> {
|
|
|
269
247
|
async function isCliModule(filePath: string): Promise<boolean> {
|
|
270
248
|
try {
|
|
271
249
|
const source = await fs.promises.readFile(filePath, 'utf-8');
|
|
272
|
-
return
|
|
250
|
+
return PLUGIN_MODULE_PATTERN.test(source);
|
|
273
251
|
} catch (err) {
|
|
274
252
|
log.warn(`Failed to inspect module ${filePath}: ${getErrorMessage(err)}`);
|
|
275
253
|
return false;
|
package/src/doctor.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
|
10
10
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
11
11
|
import { BrowserBridge } from './browser/index.js';
|
|
12
12
|
import { listSessions } from './browser/daemon-client.js';
|
|
13
|
+
import { getErrorMessage } from './errors.js';
|
|
13
14
|
|
|
14
15
|
export type DoctorOptions = {
|
|
15
16
|
fix?: boolean;
|
|
@@ -46,8 +47,8 @@ export async function checkConnectivity(opts?: { timeout?: number }): Promise<Co
|
|
|
46
47
|
await page.evaluate('1 + 1');
|
|
47
48
|
await mcp.close();
|
|
48
49
|
return { ok: true, durationMs: Date.now() - start };
|
|
49
|
-
} catch (err
|
|
50
|
-
return { ok: false, error:
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { ok: false, error: getErrorMessage(err), durationMs: Date.now() - start };
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as http from 'node:http';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
|
|
7
|
+
|
|
8
|
+
const servers: http.Server[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
|
|
12
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
13
|
+
})));
|
|
14
|
+
servers.length = 0;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'): Promise<string> {
|
|
18
|
+
const server = http.createServer(handler);
|
|
19
|
+
servers.push(server);
|
|
20
|
+
await new Promise<void>((resolve) => server.listen(0, hostname, resolve));
|
|
21
|
+
const address = server.address();
|
|
22
|
+
if (!address || typeof address === 'string') {
|
|
23
|
+
throw new Error('Failed to start test server');
|
|
24
|
+
}
|
|
25
|
+
return `http://${hostname}:${address.port}`;
|
|
26
|
+
}
|
|
3
27
|
|
|
4
28
|
describe('download helpers', () => {
|
|
5
29
|
it('resolves relative redirects against the original URL', () => {
|
|
@@ -13,4 +37,71 @@ describe('download helpers', () => {
|
|
|
13
37
|
{ name: 'ct0', value: 'def', domain: 'example.com' },
|
|
14
38
|
])).toBe('sid=abc; ct0=def');
|
|
15
39
|
});
|
|
40
|
+
|
|
41
|
+
it('fails after exceeding the redirect limit', async () => {
|
|
42
|
+
const baseUrl = await startServer((_req, res) => {
|
|
43
|
+
res.statusCode = 302;
|
|
44
|
+
res.setHeader('Location', '/loop');
|
|
45
|
+
res.end();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
|
|
49
|
+
const destPath = path.join(tempDir, 'file.txt');
|
|
50
|
+
const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
success: false,
|
|
54
|
+
size: 0,
|
|
55
|
+
error: 'Too many redirects (> 2)',
|
|
56
|
+
});
|
|
57
|
+
expect(fs.existsSync(destPath)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does not forward cookies across cross-domain redirects', async () => {
|
|
61
|
+
let forwardedCookie: string | undefined;
|
|
62
|
+
const targetUrl = await startServer((req, res) => {
|
|
63
|
+
forwardedCookie = req.headers.cookie;
|
|
64
|
+
res.statusCode = 200;
|
|
65
|
+
res.end('ok');
|
|
66
|
+
}, 'localhost');
|
|
67
|
+
|
|
68
|
+
const redirectUrl = await startServer((_req, res) => {
|
|
69
|
+
res.statusCode = 302;
|
|
70
|
+
res.setHeader('Location', targetUrl);
|
|
71
|
+
res.end();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
|
|
75
|
+
const destPath = path.join(tempDir, 'redirect.txt');
|
|
76
|
+
const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({ success: true, size: 2 });
|
|
79
|
+
expect(forwardedCookie).toBeUndefined();
|
|
80
|
+
expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not forward cookie headers across cross-domain redirects', async () => {
|
|
84
|
+
let forwardedCookie: string | undefined;
|
|
85
|
+
const targetUrl = await startServer((req, res) => {
|
|
86
|
+
forwardedCookie = req.headers.cookie;
|
|
87
|
+
res.statusCode = 200;
|
|
88
|
+
res.end('ok');
|
|
89
|
+
}, 'localhost');
|
|
90
|
+
|
|
91
|
+
const redirectUrl = await startServer((_req, res) => {
|
|
92
|
+
res.statusCode = 302;
|
|
93
|
+
res.setHeader('Location', targetUrl);
|
|
94
|
+
res.end();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
|
|
98
|
+
const destPath = path.join(tempDir, 'redirect-header.txt');
|
|
99
|
+
const result = await httpDownload(`${redirectUrl}/start`, destPath, {
|
|
100
|
+
headers: { Cookie: 'sid=header-cookie' },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result).toEqual({ success: true, size: 2 });
|
|
104
|
+
expect(forwardedCookie).toBeUndefined();
|
|
105
|
+
expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
|
|
106
|
+
});
|
|
16
107
|
});
|
package/src/download/index.ts
CHANGED
|
@@ -11,12 +11,17 @@ import * as os from 'node:os';
|
|
|
11
11
|
import { URL } from 'node:url';
|
|
12
12
|
import type { ProgressBar } from './progress.js';
|
|
13
13
|
import { isBinaryInstalled } from '../external.js';
|
|
14
|
+
import type { BrowserCookie } from '../types.js';
|
|
15
|
+
import { getErrorMessage } from '../errors.js';
|
|
16
|
+
|
|
17
|
+
export type { BrowserCookie } from '../types.js';
|
|
14
18
|
|
|
15
19
|
export interface DownloadOptions {
|
|
16
20
|
cookies?: string;
|
|
17
21
|
headers?: Record<string, string>;
|
|
18
22
|
timeout?: number;
|
|
19
23
|
onProgress?: (received: number, total: number) => void;
|
|
24
|
+
maxRedirects?: number;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export interface YtdlpOptions {
|
|
@@ -27,26 +32,11 @@ export interface YtdlpOptions {
|
|
|
27
32
|
onProgress?: (percent: number) => void;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export interface BrowserCookie {
|
|
31
|
-
name: string;
|
|
32
|
-
value: string;
|
|
33
|
-
domain: string;
|
|
34
|
-
path?: string;
|
|
35
|
-
secure?: boolean;
|
|
36
|
-
httpOnly?: boolean;
|
|
37
|
-
expirationDate?: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
35
|
/** Check if yt-dlp is available in PATH. */
|
|
41
36
|
export function checkYtdlp(): boolean {
|
|
42
37
|
return isBinaryInstalled('yt-dlp');
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
/** Check if ffmpeg is available in PATH. */
|
|
46
|
-
export function checkFfmpeg(): boolean {
|
|
47
|
-
return isBinaryInstalled('ffmpeg');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
40
|
/** Domains that host video content and can be downloaded via yt-dlp. */
|
|
51
41
|
const VIDEO_PLATFORM_DOMAINS = [
|
|
52
42
|
'youtube.com', 'youtu.be', 'bilibili.com', 'twitter.com',
|
|
@@ -92,15 +82,16 @@ export async function httpDownload(
|
|
|
92
82
|
url: string,
|
|
93
83
|
destPath: string,
|
|
94
84
|
options: DownloadOptions = {},
|
|
85
|
+
redirectCount = 0,
|
|
95
86
|
): Promise<{ success: boolean; size: number; error?: string }> {
|
|
96
|
-
const { cookies, headers = {}, timeout = 30000, onProgress } = options;
|
|
87
|
+
const { cookies, headers = {}, timeout = 30000, onProgress, maxRedirects = 10 } = options;
|
|
97
88
|
|
|
98
89
|
return new Promise((resolve) => {
|
|
99
90
|
const parsedUrl = new URL(url);
|
|
100
91
|
const protocol = parsedUrl.protocol === 'https:' ? https : http;
|
|
101
92
|
|
|
102
93
|
const requestHeaders: Record<string, string> = {
|
|
103
|
-
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
94
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
104
95
|
...headers,
|
|
105
96
|
};
|
|
106
97
|
|
|
@@ -120,7 +111,23 @@ export async function httpDownload(
|
|
|
120
111
|
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
121
112
|
file.close();
|
|
122
113
|
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
123
|
-
|
|
114
|
+
if (redirectCount >= maxRedirects) {
|
|
115
|
+
resolve({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
119
|
+
const originalHost = new URL(url).hostname;
|
|
120
|
+
const redirectHost = new URL(redirectUrl).hostname;
|
|
121
|
+
// Do not forward cookies when a redirect crosses host boundaries.
|
|
122
|
+
const redirectOptions = originalHost === redirectHost
|
|
123
|
+
? options
|
|
124
|
+
: { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
|
|
125
|
+
httpDownload(
|
|
126
|
+
redirectUrl,
|
|
127
|
+
destPath,
|
|
128
|
+
redirectOptions,
|
|
129
|
+
redirectCount + 1,
|
|
130
|
+
).then(resolve);
|
|
124
131
|
return;
|
|
125
132
|
}
|
|
126
133
|
|
|
@@ -168,6 +175,13 @@ export function resolveRedirectUrl(currentUrl: string, location: string): string
|
|
|
168
175
|
return new URL(location, currentUrl).toString();
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
function stripCookieHeaders(headers?: Record<string, string>): Record<string, string> | undefined {
|
|
179
|
+
if (!headers) return headers;
|
|
180
|
+
return Object.fromEntries(
|
|
181
|
+
Object.entries(headers).filter(([key]) => key.toLowerCase() !== 'cookie'),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
171
185
|
/**
|
|
172
186
|
* Export cookies to Netscape format for yt-dlp.
|
|
173
187
|
*/
|
|
@@ -188,7 +202,9 @@ export function exportCookiesToNetscape(
|
|
|
188
202
|
const cookiePath = cookie.path || '/';
|
|
189
203
|
const secure = cookie.secure ? 'TRUE' : 'FALSE';
|
|
190
204
|
const expiry = Math.floor(Date.now() / 1000) + 86400 * 365; // 1 year from now
|
|
191
|
-
|
|
205
|
+
const safeName = cookie.name.replace(/[\t\n\r]/g, '');
|
|
206
|
+
const safeValue = cookie.value.replace(/[\t\n\r]/g, '');
|
|
207
|
+
lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${safeName}\t${safeValue}`);
|
|
192
208
|
}
|
|
193
209
|
|
|
194
210
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -226,8 +242,13 @@ export async function ytdlpDownload(
|
|
|
226
242
|
'--progress',
|
|
227
243
|
];
|
|
228
244
|
|
|
229
|
-
if (cookiesFile
|
|
230
|
-
|
|
245
|
+
if (cookiesFile) {
|
|
246
|
+
if (fs.existsSync(cookiesFile)) {
|
|
247
|
+
args.push('--cookies', cookiesFile);
|
|
248
|
+
} else {
|
|
249
|
+
console.error(`[download] Cookies file not found: ${cookiesFile}, falling back to browser cookies`);
|
|
250
|
+
args.push('--cookies-from-browser', 'chrome');
|
|
251
|
+
}
|
|
231
252
|
} else {
|
|
232
253
|
// Try to use browser cookies
|
|
233
254
|
args.push('--cookies-from-browser', 'chrome');
|
|
@@ -319,8 +340,8 @@ export async function saveDocument(
|
|
|
319
340
|
|
|
320
341
|
fs.writeFileSync(destPath, output, 'utf-8');
|
|
321
342
|
return { success: true, size: Buffer.byteLength(output, 'utf-8') };
|
|
322
|
-
} catch (err
|
|
323
|
-
return { success: false, size: 0, error: err
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return { success: false, size: 0, error: getErrorMessage(err) };
|
|
324
345
|
}
|
|
325
346
|
}
|
|
326
347
|
|