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