@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,113 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './search.js';
|
|
4
|
+
|
|
5
|
+
describe('twitter search command', () => {
|
|
6
|
+
it('retries transient SPA navigation failures before giving up', async () => {
|
|
7
|
+
const command = getRegistry().get('twitter/search');
|
|
8
|
+
expect(command?.func).toBeTypeOf('function');
|
|
9
|
+
|
|
10
|
+
const evaluate = vi.fn()
|
|
11
|
+
.mockResolvedValueOnce(undefined)
|
|
12
|
+
.mockResolvedValueOnce('/explore')
|
|
13
|
+
.mockResolvedValueOnce(undefined)
|
|
14
|
+
.mockResolvedValueOnce('/search');
|
|
15
|
+
|
|
16
|
+
const page = {
|
|
17
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
evaluate,
|
|
21
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([
|
|
23
|
+
{
|
|
24
|
+
data: {
|
|
25
|
+
search_by_raw_query: {
|
|
26
|
+
search_timeline: {
|
|
27
|
+
timeline: {
|
|
28
|
+
instructions: [
|
|
29
|
+
{
|
|
30
|
+
type: 'TimelineAddEntries',
|
|
31
|
+
entries: [
|
|
32
|
+
{
|
|
33
|
+
entryId: 'tweet-1',
|
|
34
|
+
content: {
|
|
35
|
+
itemContent: {
|
|
36
|
+
tweet_results: {
|
|
37
|
+
result: {
|
|
38
|
+
rest_id: '1',
|
|
39
|
+
legacy: {
|
|
40
|
+
full_text: 'hello world',
|
|
41
|
+
favorite_count: 7,
|
|
42
|
+
},
|
|
43
|
+
core: {
|
|
44
|
+
user_results: {
|
|
45
|
+
result: {
|
|
46
|
+
core: {
|
|
47
|
+
screen_name: 'alice',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
views: {
|
|
53
|
+
count: '12',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = await command!.func!(page as any, { query: 'from:alice', limit: 5 });
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual([
|
|
74
|
+
{
|
|
75
|
+
id: '1',
|
|
76
|
+
author: 'alice',
|
|
77
|
+
text: 'hello world',
|
|
78
|
+
likes: 7,
|
|
79
|
+
views: '12',
|
|
80
|
+
url: 'https://x.com/i/status/1',
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
expect(page.installInterceptor).toHaveBeenCalledWith('SearchTimeline');
|
|
84
|
+
expect(evaluate).toHaveBeenCalledTimes(4);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('throws with the final path after both attempts fail', async () => {
|
|
88
|
+
const command = getRegistry().get('twitter/search');
|
|
89
|
+
expect(command?.func).toBeTypeOf('function');
|
|
90
|
+
|
|
91
|
+
const evaluate = vi.fn()
|
|
92
|
+
.mockResolvedValueOnce(undefined)
|
|
93
|
+
.mockResolvedValueOnce('/explore')
|
|
94
|
+
.mockResolvedValueOnce(undefined)
|
|
95
|
+
.mockResolvedValueOnce('/login');
|
|
96
|
+
|
|
97
|
+
const page = {
|
|
98
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
99
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
100
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
101
|
+
evaluate,
|
|
102
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
103
|
+
getInterceptedRequests: vi.fn(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await expect(command!.func!(page as any, { query: 'from:alice', limit: 5 }))
|
|
107
|
+
.rejects
|
|
108
|
+
.toThrow('Final path: /login');
|
|
109
|
+
expect(page.autoScroll).not.toHaveBeenCalled();
|
|
110
|
+
expect(page.getInterceptedRequests).not.toHaveBeenCalled();
|
|
111
|
+
expect(evaluate).toHaveBeenCalledTimes(4);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -1,4 +1,41 @@
|
|
|
1
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
1
2
|
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Trigger Twitter search SPA navigation and retry once on transient failures.
|
|
7
|
+
*
|
|
8
|
+
* Twitter/X sometimes keeps the page on /explore for a short period even after
|
|
9
|
+
* pushState + popstate. A second attempt is enough for the intermittent cases
|
|
10
|
+
* reported in issue #353 while keeping the flow narrowly scoped.
|
|
11
|
+
*/
|
|
12
|
+
async function navigateToSearch(page: Pick<IPage, 'evaluate' | 'wait'>, query: string): Promise<void> {
|
|
13
|
+
const searchUrl = JSON.stringify(`/search?q=${encodeURIComponent(query)}&f=top`);
|
|
14
|
+
let lastPath = '';
|
|
15
|
+
|
|
16
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
17
|
+
await page.evaluate(`
|
|
18
|
+
(() => {
|
|
19
|
+
window.history.pushState({}, '', ${searchUrl});
|
|
20
|
+
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
|
|
21
|
+
})()
|
|
22
|
+
`);
|
|
23
|
+
await page.wait(5);
|
|
24
|
+
|
|
25
|
+
lastPath = String(await page.evaluate('() => window.location.pathname') || '');
|
|
26
|
+
if (lastPath.startsWith('/search')) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (attempt < 2) {
|
|
31
|
+
await page.wait(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new CommandExecutionError(
|
|
36
|
+
`SPA navigation to /search failed. Final path: ${lastPath || '(empty)'}. Twitter may have changed its routing.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
2
39
|
|
|
3
40
|
cli({
|
|
4
41
|
site: 'twitter',
|
|
@@ -29,20 +66,7 @@ cli({
|
|
|
29
66
|
// a full page reload, so the interceptor stays alive.
|
|
30
67
|
// Note: the previous approach (nativeSetter + Enter keydown on the
|
|
31
68
|
// search input) does not reliably trigger Twitter's form submission.
|
|
32
|
-
|
|
33
|
-
await page.evaluate(`
|
|
34
|
-
(() => {
|
|
35
|
-
window.history.pushState({}, '', ${searchUrl});
|
|
36
|
-
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
|
|
37
|
-
})()
|
|
38
|
-
`);
|
|
39
|
-
await page.wait(5);
|
|
40
|
-
|
|
41
|
-
// Verify SPA navigation succeeded
|
|
42
|
-
const currentPath = await page.evaluate('() => window.location.pathname');
|
|
43
|
-
if (!currentPath?.startsWith('/search')) {
|
|
44
|
-
throw new Error('SPA navigation to /search failed. Twitter may have changed its routing.');
|
|
45
|
-
}
|
|
69
|
+
await navigateToSearch(page, query);
|
|
46
70
|
|
|
47
71
|
// 4. Scroll to trigger additional pagination
|
|
48
72
|
await page.autoScroll({ times: 3, delayMs: 2000 });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import { AuthRequiredError } from '../../errors.js';
|
|
2
|
+
import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
|
|
3
3
|
|
|
4
4
|
// ── Twitter GraphQL constants ──────────────────────────────────────────
|
|
5
5
|
|
|
@@ -165,7 +165,7 @@ cli({
|
|
|
165
165
|
}`);
|
|
166
166
|
|
|
167
167
|
if (data?.error) {
|
|
168
|
-
if (allTweets.length === 0) throw new
|
|
168
|
+
if (allTweets.length === 0) throw new CommandExecutionError(`HTTP ${data.error}: Tweet not found or queryId expired`);
|
|
169
169
|
break;
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
|
|
1
2
|
import { cli, Strategy } from '../../registry.js';
|
|
2
3
|
|
|
3
4
|
// ── Twitter GraphQL constants ──────────────────────────────────────────
|
|
@@ -194,7 +195,7 @@ cli({
|
|
|
194
195
|
const ct0 = await page.evaluate(`() => {
|
|
195
196
|
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
|
|
196
197
|
}`);
|
|
197
|
-
if (!ct0) throw new
|
|
198
|
+
if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
198
199
|
|
|
199
200
|
// Dynamically resolve queryId for the selected endpoint
|
|
200
201
|
const resolved = await page.evaluate(`async () => {
|
|
@@ -236,7 +237,7 @@ cli({
|
|
|
236
237
|
|
|
237
238
|
if (data?.error) {
|
|
238
239
|
if (allTweets.length === 0)
|
|
239
|
-
throw new
|
|
240
|
+
throw new CommandExecutionError(`HTTP ${data.error}: Failed to fetch timeline. queryId may have expired.`);
|
|
240
241
|
break;
|
|
241
242
|
}
|
|
242
243
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
2
3
|
|
|
3
4
|
// ── Twitter GraphQL constants ──────────────────────────────────────────
|
|
4
5
|
|
|
@@ -37,7 +38,7 @@ cli({
|
|
|
37
38
|
const ct0 = await page.evaluate(`(() => {
|
|
38
39
|
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
|
|
39
40
|
})()`);
|
|
40
|
-
if (!ct0) throw new
|
|
41
|
+
if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
41
42
|
|
|
42
43
|
// Try legacy guide.json API first (faster than DOM scraping)
|
|
43
44
|
let trends: TrendItem[] = [];
|
|
@@ -105,7 +106,7 @@ cli({
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
if (trends.length === 0) {
|
|
108
|
-
throw new
|
|
109
|
+
throw new EmptyResultError('twitter trending', 'API may have changed or login may be required.');
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
return trends.slice(0, limit);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
1
2
|
import { cli, Strategy } from '../../registry.js';
|
|
2
3
|
import type { IPage } from '../../types.js';
|
|
3
4
|
|
|
@@ -13,7 +14,7 @@ cli({
|
|
|
13
14
|
],
|
|
14
15
|
columns: ['status', 'message'],
|
|
15
16
|
func: async (page: IPage | null, kwargs: any) => {
|
|
16
|
-
if (!page) throw new
|
|
17
|
+
if (!page) throw new CommandExecutionError('Browser session required for twitter unblock');
|
|
17
18
|
const username = kwargs.username.replace(/^@/, '');
|
|
18
19
|
|
|
19
20
|
await page.goto(`https://x.com/${username}`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
1
2
|
import { cli, Strategy } from '../../registry.js';
|
|
2
3
|
import type { IPage } from '../../types.js';
|
|
3
4
|
|
|
@@ -13,7 +14,7 @@ cli({
|
|
|
13
14
|
],
|
|
14
15
|
columns: ['status', 'message'],
|
|
15
16
|
func: async (page: IPage | null, kwargs: any) => {
|
|
16
|
-
if (!page) throw new
|
|
17
|
+
if (!page) throw new CommandExecutionError('Browser session required for twitter unbookmark');
|
|
17
18
|
|
|
18
19
|
await page.goto(kwargs.url);
|
|
19
20
|
await page.wait(5);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
2
3
|
import type { IPage } from '../../types.js';
|
|
3
4
|
|
|
4
5
|
cli({
|
|
@@ -13,7 +14,7 @@ cli({
|
|
|
13
14
|
],
|
|
14
15
|
columns: ['status', 'message'],
|
|
15
16
|
func: async (page: IPage | null, kwargs: any) => {
|
|
16
|
-
if (!page) throw new
|
|
17
|
+
if (!page) throw new CommandExecutionError('Browser session required for twitter unfollow');
|
|
17
18
|
const username = kwargs.username.replace(/^@/, '');
|
|
18
19
|
|
|
19
20
|
await page.goto(`https://x.com/${username}`);
|
package/src/clis/v2ex/daily.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* V2EX Daily Check-in adapter.
|
|
3
3
|
*/
|
|
4
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
4
5
|
import { cli, Strategy } from '../../registry.js';
|
|
5
6
|
import type { IPage } from '../../types.js';
|
|
6
7
|
|
|
@@ -15,7 +16,7 @@ cli({
|
|
|
15
16
|
args: [],
|
|
16
17
|
columns: ['status', 'message'],
|
|
17
18
|
func: async (page: IPage | null) => {
|
|
18
|
-
if (!page) throw new
|
|
19
|
+
if (!page) throw new CommandExecutionError('Browser page required');
|
|
19
20
|
|
|
20
21
|
if (process.env.OPENCLI_VERBOSE) {
|
|
21
22
|
console.error('[opencli:v2ex] Navigating to /mission/daily');
|
|
@@ -60,7 +61,7 @@ cli({
|
|
|
60
61
|
console.error(`[opencli:v2ex:debug] Page Title: ${checkResult.debug_title}`);
|
|
61
62
|
console.error(`[opencli:v2ex:debug] Page Body: ${checkResult.debug_body}`);
|
|
62
63
|
}
|
|
63
|
-
throw new
|
|
64
|
+
throw new CommandExecutionError(checkResult.error);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
if (checkResult.claimed) {
|
package/src/clis/v2ex/me.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* V2EX Me (Profile/Balance) adapter.
|
|
3
3
|
*/
|
|
4
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
4
5
|
import { cli, Strategy } from '../../registry.js';
|
|
5
6
|
import type { IPage } from '../../types.js';
|
|
6
7
|
|
|
@@ -15,7 +16,7 @@ cli({
|
|
|
15
16
|
args: [],
|
|
16
17
|
columns: ['username', 'balance', 'unread_notifications', 'daily_reward_ready'],
|
|
17
18
|
func: async (page: IPage | null) => {
|
|
18
|
-
if (!page) throw new
|
|
19
|
+
if (!page) throw new CommandExecutionError('Browser page required');
|
|
19
20
|
|
|
20
21
|
if (process.env.OPENCLI_VERBOSE) {
|
|
21
22
|
console.error('[opencli:v2ex] Navigating to /');
|
|
@@ -95,7 +96,7 @@ cli({
|
|
|
95
96
|
console.error(`[opencli:v2ex:debug] Page Title: ${data.debug_title}`);
|
|
96
97
|
console.error(`[opencli:v2ex:debug] Page Body: ${data.debug_body}`);
|
|
97
98
|
}
|
|
98
|
-
throw new
|
|
99
|
+
throw new CommandExecutionError(data.error);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
return [data];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* V2EX Notifications adapter.
|
|
3
3
|
*/
|
|
4
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
4
5
|
import { cli, Strategy } from '../../registry.js';
|
|
5
6
|
import type { IPage } from '../../types.js';
|
|
6
7
|
|
|
@@ -17,7 +18,7 @@ cli({
|
|
|
17
18
|
],
|
|
18
19
|
columns: ['type', 'content', 'time'],
|
|
19
20
|
func: async (page: IPage | null, kwargs) => {
|
|
20
|
-
if (!page) throw new
|
|
21
|
+
if (!page) throw new CommandExecutionError('Browser page required');
|
|
21
22
|
|
|
22
23
|
if (process.env.OPENCLI_VERBOSE) {
|
|
23
24
|
console.error('[opencli:v2ex] Navigating to /notifications');
|
|
@@ -67,9 +68,7 @@ cli({
|
|
|
67
68
|
}
|
|
68
69
|
`);
|
|
69
70
|
|
|
70
|
-
if (!Array.isArray(data))
|
|
71
|
-
throw new Error('Failed to parse notifications data');
|
|
72
|
-
}
|
|
71
|
+
if (!Array.isArray(data)) throw new CommandExecutionError('Failed to parse notifications data');
|
|
73
72
|
|
|
74
73
|
const limit = kwargs.limit || 20;
|
|
75
74
|
return data.slice(0, limit);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic web page reader — fetch any URL and export as Markdown.
|
|
3
|
+
*
|
|
4
|
+
* Uses browser-side DOM heuristics to extract the main content:
|
|
5
|
+
* 1. <article> element
|
|
6
|
+
* 2. [role="main"] element
|
|
7
|
+
* 3. <main> element
|
|
8
|
+
* 4. Largest text-dense block as fallback
|
|
9
|
+
*
|
|
10
|
+
* Pipes through the shared article-download pipeline (Turndown + image download).
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* opencli web read --url "https://www.anthropic.com/research/..." --output ./articles
|
|
14
|
+
* opencli web read --url "https://..." --download-images false
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { cli, Strategy } from '../../registry.js';
|
|
18
|
+
import { downloadArticle } from '../../download/article-download.js';
|
|
19
|
+
|
|
20
|
+
cli({
|
|
21
|
+
site: 'web',
|
|
22
|
+
name: 'read',
|
|
23
|
+
description: 'Fetch any web page and export as Markdown',
|
|
24
|
+
strategy: Strategy.COOKIE,
|
|
25
|
+
navigateBefore: false, // we handle navigation ourselves
|
|
26
|
+
args: [
|
|
27
|
+
{ name: 'url', required: true, help: 'Any web page URL' },
|
|
28
|
+
{ name: 'output', default: './web-articles', help: 'Output directory' },
|
|
29
|
+
{ name: 'download-images', type: 'boolean', default: true, help: 'Download images locally' },
|
|
30
|
+
{ name: 'wait', type: 'int', default: 3, help: 'Seconds to wait after page load' },
|
|
31
|
+
],
|
|
32
|
+
columns: ['title', 'author', 'publish_time', 'status', 'size'],
|
|
33
|
+
func: async (page, kwargs) => {
|
|
34
|
+
const url = kwargs.url;
|
|
35
|
+
const waitSeconds = kwargs.wait ?? 3;
|
|
36
|
+
|
|
37
|
+
// Navigate to the target URL
|
|
38
|
+
await page.goto(url);
|
|
39
|
+
await page.wait(waitSeconds);
|
|
40
|
+
|
|
41
|
+
// Extract article content using browser-side heuristics
|
|
42
|
+
const data = await page.evaluate(`
|
|
43
|
+
(() => {
|
|
44
|
+
const result = {
|
|
45
|
+
title: '',
|
|
46
|
+
author: '',
|
|
47
|
+
publishTime: '',
|
|
48
|
+
contentHtml: '',
|
|
49
|
+
imageUrls: []
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// --- Title extraction ---
|
|
53
|
+
// Priority: og:title > <title> > first <h1>
|
|
54
|
+
const ogTitle = document.querySelector('meta[property="og:title"]');
|
|
55
|
+
if (ogTitle) {
|
|
56
|
+
result.title = ogTitle.getAttribute('content')?.trim() || '';
|
|
57
|
+
}
|
|
58
|
+
if (!result.title) {
|
|
59
|
+
result.title = document.title?.trim() || '';
|
|
60
|
+
}
|
|
61
|
+
if (!result.title) {
|
|
62
|
+
const h1 = document.querySelector('h1');
|
|
63
|
+
result.title = h1?.textContent?.trim() || 'untitled';
|
|
64
|
+
}
|
|
65
|
+
// Strip site suffix (e.g. " | Anthropic", " - Blog")
|
|
66
|
+
result.title = result.title.replace(/\\s*[|\\-–—]\\s*[^|\\-–—]{1,30}$/, '').trim();
|
|
67
|
+
|
|
68
|
+
// --- Author extraction ---
|
|
69
|
+
const authorMeta = document.querySelector(
|
|
70
|
+
'meta[name="author"], meta[property="article:author"], meta[name="twitter:creator"]'
|
|
71
|
+
);
|
|
72
|
+
result.author = authorMeta?.getAttribute('content')?.trim() || '';
|
|
73
|
+
|
|
74
|
+
// --- Publish time extraction ---
|
|
75
|
+
const timeMeta = document.querySelector(
|
|
76
|
+
'meta[property="article:published_time"], meta[name="date"], meta[name="publishdate"], time[datetime]'
|
|
77
|
+
);
|
|
78
|
+
if (timeMeta) {
|
|
79
|
+
result.publishTime = timeMeta.getAttribute('content')
|
|
80
|
+
|| timeMeta.getAttribute('datetime')
|
|
81
|
+
|| timeMeta.textContent?.trim()
|
|
82
|
+
|| '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- Content extraction ---
|
|
86
|
+
// Strategy: try semantic elements first, then fall back to largest text block
|
|
87
|
+
let contentEl = null;
|
|
88
|
+
|
|
89
|
+
// 1. <article>
|
|
90
|
+
const articles = document.querySelectorAll('article');
|
|
91
|
+
if (articles.length === 1) {
|
|
92
|
+
contentEl = articles[0];
|
|
93
|
+
} else if (articles.length > 1) {
|
|
94
|
+
// Pick the largest article by text length
|
|
95
|
+
let maxLen = 0;
|
|
96
|
+
articles.forEach(a => {
|
|
97
|
+
const len = a.textContent?.length || 0;
|
|
98
|
+
if (len > maxLen) { maxLen = len; contentEl = a; }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. [role="main"]
|
|
103
|
+
if (!contentEl) {
|
|
104
|
+
contentEl = document.querySelector('[role="main"]');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. <main>
|
|
108
|
+
if (!contentEl) {
|
|
109
|
+
contentEl = document.querySelector('main');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 4. Largest text-dense block fallback
|
|
113
|
+
if (!contentEl) {
|
|
114
|
+
const candidates = document.querySelectorAll(
|
|
115
|
+
'div[class*="content"], div[class*="article"], div[class*="post"], ' +
|
|
116
|
+
'div[class*="entry"], div[class*="body"], div[id*="content"], ' +
|
|
117
|
+
'div[id*="article"], div[id*="post"], section'
|
|
118
|
+
);
|
|
119
|
+
let maxLen = 0;
|
|
120
|
+
candidates.forEach(c => {
|
|
121
|
+
const len = c.textContent?.length || 0;
|
|
122
|
+
if (len > maxLen) { maxLen = len; contentEl = c; }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 5. Last resort: document.body
|
|
127
|
+
if (!contentEl || (contentEl.textContent?.length || 0) < 200) {
|
|
128
|
+
contentEl = document.body;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Clean up noise elements before extraction
|
|
132
|
+
const clone = contentEl.cloneNode(true);
|
|
133
|
+
const noise = 'nav, header, footer, aside, .sidebar, .nav, .menu, .footer, ' +
|
|
134
|
+
'.header, .comments, .comment, .ad, .ads, .advertisement, .social-share, ' +
|
|
135
|
+
'.related-posts, .newsletter, .cookie-banner, script, style, noscript, iframe';
|
|
136
|
+
clone.querySelectorAll(noise).forEach(el => el.remove());
|
|
137
|
+
|
|
138
|
+
// Deduplicate: some sites (e.g. Anthropic) render each paragraph twice
|
|
139
|
+
// (a visible version + a line-broken animation version with missing spaces).
|
|
140
|
+
// Compare by stripping ALL whitespace so "Hello world" matches "Helloworld".
|
|
141
|
+
const stripWS = (s) => (s || '').replace(/\\s+/g, '');
|
|
142
|
+
const dedup = (parent) => {
|
|
143
|
+
const children = Array.from(parent.children || []);
|
|
144
|
+
for (let i = children.length - 1; i >= 1; i--) {
|
|
145
|
+
const curRaw = children[i].textContent || '';
|
|
146
|
+
const prevRaw = children[i - 1].textContent || '';
|
|
147
|
+
const cur = stripWS(curRaw);
|
|
148
|
+
const prev = stripWS(prevRaw);
|
|
149
|
+
if (cur.length < 20 || prev.length < 20) continue;
|
|
150
|
+
// Exact match after whitespace strip, or >90% overlap
|
|
151
|
+
if (cur === prev) {
|
|
152
|
+
// Keep the one with more proper spacing (more spaces = better formatted)
|
|
153
|
+
const curSpaces = (curRaw.match(/ /g) || []).length;
|
|
154
|
+
const prevSpaces = (prevRaw.match(/ /g) || []).length;
|
|
155
|
+
if (curSpaces >= prevSpaces) children[i - 1].remove();
|
|
156
|
+
else children[i].remove();
|
|
157
|
+
} else if (prev.includes(cur) && cur.length / prev.length > 0.8) {
|
|
158
|
+
children[i].remove();
|
|
159
|
+
} else if (cur.includes(prev) && prev.length / cur.length > 0.8) {
|
|
160
|
+
children[i - 1].remove();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
dedup(clone);
|
|
165
|
+
clone.querySelectorAll('section, div').forEach(el => {
|
|
166
|
+
if (el.children && el.children.length > 2) dedup(el);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
result.contentHtml = clone.innerHTML;
|
|
170
|
+
|
|
171
|
+
// --- Image extraction ---
|
|
172
|
+
const seen = new Set();
|
|
173
|
+
clone.querySelectorAll('img').forEach(img => {
|
|
174
|
+
const src = img.getAttribute('data-src')
|
|
175
|
+
|| img.getAttribute('data-original')
|
|
176
|
+
|| img.getAttribute('src');
|
|
177
|
+
if (src && !src.startsWith('data:') && !seen.has(src)) {
|
|
178
|
+
seen.add(src);
|
|
179
|
+
result.imageUrls.push(src);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
})()
|
|
185
|
+
`);
|
|
186
|
+
|
|
187
|
+
// Determine Referer from URL for image downloads
|
|
188
|
+
let referer = '';
|
|
189
|
+
try {
|
|
190
|
+
const parsed = new URL(url);
|
|
191
|
+
referer = parsed.origin + '/';
|
|
192
|
+
} catch { /* ignore */ }
|
|
193
|
+
|
|
194
|
+
return downloadArticle(
|
|
195
|
+
{
|
|
196
|
+
title: data?.title || 'untitled',
|
|
197
|
+
author: data?.author,
|
|
198
|
+
publishTime: data?.publishTime,
|
|
199
|
+
sourceUrl: url,
|
|
200
|
+
contentHtml: data?.contentHtml || '',
|
|
201
|
+
imageUrls: data?.imageUrls,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
output: kwargs.output,
|
|
205
|
+
downloadImages: kwargs['download-images'],
|
|
206
|
+
imageHeaders: referer ? { Referer: referer } : undefined,
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { fetchDanjuanAll } from './danjuan-utils.js';
|
|
3
|
+
|
|
4
|
+
describe('fetchDanjuanAll', () => {
|
|
5
|
+
it('throws when no Danjuan accounts are visible', async () => {
|
|
6
|
+
const mockPage = {
|
|
7
|
+
evaluate: vi.fn().mockResolvedValue({ _emptyAccounts: true }),
|
|
8
|
+
} as any;
|
|
9
|
+
|
|
10
|
+
await expect(fetchDanjuanAll(mockPage)).rejects.toThrow('No fund accounts found');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('throws when any account detail request fails', async () => {
|
|
14
|
+
const mockPage = {
|
|
15
|
+
evaluate: vi.fn().mockResolvedValue({
|
|
16
|
+
detailErrors: [
|
|
17
|
+
{ accountName: '默认账户', accountId: 'acc-1', error: 403 },
|
|
18
|
+
],
|
|
19
|
+
}),
|
|
20
|
+
} as any;
|
|
21
|
+
|
|
22
|
+
await expect(fetchDanjuanAll(mockPage)).rejects.toThrow(
|
|
23
|
+
'Failed to fetch Danjuan account details: 默认账户 (acc-1): 403',
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns the combined snapshot when all account details succeed', async () => {
|
|
28
|
+
const snapshot = {
|
|
29
|
+
asOf: '2026-03-25',
|
|
30
|
+
totalAssetAmount: 100,
|
|
31
|
+
totalAssetDailyGain: 1,
|
|
32
|
+
totalAssetHoldGain: 2,
|
|
33
|
+
totalAssetTotalGain: 3,
|
|
34
|
+
totalFundMarketValue: 80,
|
|
35
|
+
accounts: [{ accountId: 'acc-1', accountName: '默认账户' }],
|
|
36
|
+
holdings: [{ accountId: 'acc-1', fdCode: '000001', fdName: '示例基金' }],
|
|
37
|
+
detailErrors: [],
|
|
38
|
+
};
|
|
39
|
+
const mockPage = {
|
|
40
|
+
evaluate: vi.fn().mockResolvedValue(snapshot),
|
|
41
|
+
} as any;
|
|
42
|
+
|
|
43
|
+
await expect(fetchDanjuanAll(mockPage)).resolves.toMatchObject({
|
|
44
|
+
asOf: '2026-03-25',
|
|
45
|
+
accounts: [{ accountId: 'acc-1', accountName: '默认账户' }],
|
|
46
|
+
holdings: [{ accountId: 'acc-1', fdCode: '000001', fdName: '示例基金' }],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|