@jackwener/opencli 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/pull_request_template.md +3 -1
- package/.github/workflows/build-extension.yml +7 -1
- package/.github/workflows/ci.yml +29 -3
- package/.github/workflows/docs.yml +1 -1
- package/.github/workflows/e2e-headed.yml +20 -0
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/security.yml +0 -3
- package/CHANGELOG.md +55 -0
- package/CONTRIBUTING.md +6 -3
- package/README.md +30 -3
- package/README.zh-CN.md +30 -3
- package/SKILL.md +7 -1
- package/TESTING.md +1 -0
- package/chatwise-opencli.ps1 +82 -0
- package/dist/analysis.d.ts +38 -0
- package/dist/analysis.js +166 -0
- package/dist/browser/cdp.d.ts +0 -4
- package/dist/browser/cdp.js +53 -41
- package/dist/browser/cdp.test.d.ts +1 -0
- package/dist/browser/cdp.test.js +52 -0
- package/dist/browser/dom-snapshot.d.ts +2 -2
- package/dist/browser/dom-snapshot.js +54 -1
- package/dist/browser/dom-snapshot.test.js +36 -0
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/mcp.d.ts +0 -2
- package/dist/browser/mcp.js +2 -3
- package/dist/browser/page.d.ts +4 -3
- package/dist/browser/page.js +34 -37
- package/dist/browser/stealth.d.ts +0 -2
- package/dist/browser/stealth.js +24 -9
- package/dist/browser.test.js +2 -2
- package/dist/build-manifest.js +15 -9
- package/dist/build-manifest.test.js +12 -0
- package/dist/cascade.js +4 -2
- package/dist/cli-manifest.json +639 -258
- package/dist/cli.js +57 -29
- package/dist/clis/_shared/desktop-commands.d.ts +22 -0
- package/dist/clis/_shared/desktop-commands.js +108 -0
- package/dist/clis/antigravity/serve.js +5 -2
- package/dist/clis/arxiv/search.js +1 -1
- package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
- package/dist/clis/bilibili/dynamic.test.js +68 -0
- package/dist/clis/bilibili/favorite.js +4 -2
- package/dist/clis/bilibili/following.js +3 -2
- package/dist/clis/bilibili/subtitle.js +8 -7
- package/dist/clis/bilibili/utils.js +2 -2
- package/dist/clis/boss/batchgreet.js +1 -1
- package/dist/clis/boss/chatlist.js +1 -1
- package/dist/clis/boss/chatmsg.js +1 -1
- package/dist/clis/boss/detail.js +1 -1
- package/dist/clis/boss/exchange.js +1 -1
- package/dist/clis/boss/greet.js +1 -1
- package/dist/clis/boss/invite.js +1 -1
- package/dist/clis/boss/joblist.js +1 -1
- package/dist/clis/boss/mark.js +4 -3
- package/dist/clis/boss/recommend.js +1 -1
- package/dist/clis/boss/resume.js +1 -1
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +5 -4
- package/dist/clis/boss/stats.js +1 -1
- package/dist/clis/chatgpt/ask.js +4 -0
- package/dist/clis/chatgpt/new.js +5 -1
- package/dist/clis/chatgpt/read.js +5 -1
- package/dist/clis/chatgpt/send.js +2 -1
- package/dist/clis/chatgpt/status.js +5 -1
- package/dist/clis/chatwise/ask.js +8 -2
- package/dist/clis/chatwise/export.js +2 -0
- package/dist/clis/chatwise/history.js +2 -0
- package/dist/clis/chatwise/model.js +8 -3
- package/dist/clis/chatwise/new.js +3 -18
- package/dist/clis/chatwise/read.js +2 -0
- package/dist/clis/chatwise/screenshot.js +3 -27
- package/dist/clis/chatwise/send.js +8 -2
- package/dist/clis/chatwise/shared.d.ts +2 -0
- package/dist/clis/chatwise/shared.js +6 -0
- package/dist/clis/chatwise/status.js +3 -22
- package/dist/clis/codex/ask.js +6 -2
- package/dist/clis/codex/dump.js +2 -25
- package/dist/clis/codex/new.js +2 -25
- package/dist/clis/codex/screenshot.js +2 -27
- package/dist/clis/codex/send.js +6 -4
- package/dist/clis/codex/status.js +2 -22
- package/dist/clis/cursor/ask.js +2 -1
- package/dist/clis/cursor/composer.js +2 -1
- package/dist/clis/cursor/dump.js +2 -25
- package/dist/clis/cursor/new.js +2 -18
- package/dist/clis/cursor/read.js +2 -1
- package/dist/clis/cursor/screenshot.js +1 -30
- package/dist/clis/cursor/send.js +2 -1
- package/dist/clis/cursor/status.js +2 -21
- package/dist/clis/dictionary/examples.yaml +25 -0
- package/dist/clis/dictionary/search.yaml +27 -0
- package/dist/clis/dictionary/synonyms.yaml +25 -0
- package/dist/clis/douban/book-hot.js +1 -1
- package/dist/clis/douban/movie-hot.js +1 -1
- package/dist/clis/douban/search.js +1 -1
- package/dist/clis/douban/utils.d.ts +4 -1
- package/dist/clis/douban/utils.js +156 -1
- package/dist/clis/doubao/ask.js +1 -1
- package/dist/clis/doubao/new.js +1 -1
- package/dist/clis/doubao/read.js +1 -1
- package/dist/clis/doubao/send.js +1 -1
- package/dist/clis/doubao/status.js +1 -1
- package/dist/clis/doubao-app/ask.js +1 -1
- package/dist/clis/doubao-app/new.js +1 -1
- package/dist/clis/doubao-app/read.js +1 -1
- package/dist/clis/doubao-app/send.js +1 -1
- package/dist/clis/grok/ask.d.ts +4 -0
- package/dist/clis/grok/ask.js +28 -10
- package/dist/clis/grok/ask.test.js +18 -0
- package/dist/clis/jd/item.d.ts +1 -0
- package/dist/clis/jd/item.js +96 -0
- package/dist/clis/jd/item.test.d.ts +1 -0
- package/dist/clis/jd/item.test.js +28 -0
- package/dist/clis/jike/feed.js +1 -1
- package/dist/clis/jike/search.js +1 -1
- package/dist/clis/linkedin/search.js +5 -4
- package/dist/clis/linkedin/timeline.d.ts +21 -0
- package/dist/clis/linkedin/timeline.js +503 -0
- package/dist/clis/linkedin/timeline.test.d.ts +1 -0
- package/dist/clis/linkedin/timeline.test.js +81 -0
- package/dist/clis/medium/feed.js +1 -1
- package/dist/clis/medium/search.js +1 -1
- package/dist/clis/medium/user.js +1 -1
- package/dist/clis/medium/{shared.js → utils.js} +2 -1
- package/dist/clis/pixiv/detail.yaml +49 -0
- package/dist/clis/pixiv/download.d.ts +7 -0
- package/dist/clis/pixiv/download.js +78 -0
- package/dist/clis/pixiv/download.test.d.ts +1 -0
- package/dist/clis/pixiv/download.test.js +87 -0
- package/dist/clis/pixiv/illusts.d.ts +8 -0
- package/dist/clis/pixiv/illusts.js +65 -0
- package/dist/clis/pixiv/illusts.test.d.ts +1 -0
- package/dist/clis/pixiv/illusts.test.js +99 -0
- package/dist/clis/pixiv/ranking.yaml +53 -0
- package/dist/clis/pixiv/search.d.ts +6 -0
- package/dist/clis/pixiv/search.js +43 -0
- package/dist/clis/pixiv/search.test.d.ts +1 -0
- package/dist/clis/pixiv/search.test.js +83 -0
- package/dist/clis/pixiv/test-utils.d.ts +12 -0
- package/dist/clis/pixiv/test-utils.js +23 -0
- package/dist/clis/pixiv/user.yaml +46 -0
- package/dist/clis/pixiv/utils.d.ts +27 -0
- package/dist/clis/pixiv/utils.js +49 -0
- package/dist/clis/reddit/comment.js +2 -1
- package/dist/clis/reddit/read.js +4 -3
- package/dist/clis/reddit/read.test.d.ts +1 -0
- package/dist/clis/reddit/read.test.js +28 -0
- package/dist/clis/reddit/save.js +2 -1
- package/dist/clis/reddit/saved.js +7 -3
- package/dist/clis/reddit/subscribe.js +2 -1
- package/dist/clis/reddit/upvote.js +2 -1
- package/dist/clis/reddit/upvoted.js +7 -3
- package/dist/clis/sinablog/article.js +1 -1
- package/dist/clis/sinablog/hot.js +1 -1
- package/dist/clis/sinablog/user.js +1 -1
- package/dist/clis/substack/feed.js +1 -1
- package/dist/clis/substack/publication.js +1 -1
- package/dist/clis/substack/search.js +3 -2
- package/dist/clis/substack/{shared.js → utils.js} +3 -2
- package/dist/clis/tiktok/search.yaml +2 -1
- package/dist/clis/twitter/accept.js +2 -1
- package/dist/clis/twitter/article.js +4 -1
- package/dist/clis/twitter/block.js +2 -1
- package/dist/clis/twitter/bookmark.js +2 -1
- package/dist/clis/twitter/bookmarks.js +3 -2
- package/dist/clis/twitter/delete.js +2 -1
- package/dist/clis/twitter/follow.js +2 -1
- package/dist/clis/twitter/followers.js +3 -2
- package/dist/clis/twitter/following.js +3 -2
- package/dist/clis/twitter/hide-reply.js +2 -1
- package/dist/clis/twitter/like.js +2 -1
- package/dist/clis/twitter/notifications.js +2 -1
- package/dist/clis/twitter/post.js +2 -1
- package/dist/clis/twitter/profile.js +5 -2
- package/dist/clis/twitter/reply-dm.js +2 -1
- package/dist/clis/twitter/reply.js +2 -1
- package/dist/clis/twitter/search.js +30 -13
- package/dist/clis/twitter/search.test.d.ts +1 -0
- package/dist/clis/twitter/search.test.js +104 -0
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/timeline.js +3 -2
- package/dist/clis/twitter/trending.js +3 -2
- package/dist/clis/twitter/unblock.js +2 -1
- package/dist/clis/twitter/unbookmark.js +2 -1
- package/dist/clis/twitter/unfollow.js +2 -1
- package/dist/clis/v2ex/daily.js +3 -2
- package/dist/clis/v2ex/me.js +3 -2
- package/dist/clis/v2ex/notifications.js +4 -4
- package/dist/clis/web/read.d.ts +16 -0
- package/dist/clis/web/read.js +202 -0
- package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
- package/dist/clis/xueqiu/danjuan-utils.js +126 -0
- package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
- package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
- package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
- package/dist/clis/xueqiu/fund-holdings.js +28 -0
- package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
- package/dist/clis/xueqiu/fund-snapshot.js +25 -0
- package/dist/clis/youtube/transcript.js +5 -4
- package/dist/clis/youtube/video.js +3 -2
- package/dist/daemon.js +7 -3
- package/dist/discovery.js +11 -10
- package/dist/doctor.js +2 -1
- package/dist/download/index.d.ts +4 -12
- package/dist/download/index.js +33 -12
- package/dist/download/index.test.js +79 -2
- package/dist/download/media-download.js +4 -2
- package/dist/engine.test.js +76 -4
- package/dist/execution.d.ts +1 -9
- package/dist/execution.js +56 -46
- package/dist/explore.js +12 -111
- package/dist/external-clis.yaml +0 -8
- package/dist/external.js +7 -5
- package/dist/external.test.js +4 -0
- package/dist/generate.d.ts +0 -9
- package/dist/generate.js +4 -20
- package/dist/hooks.d.ts +46 -0
- package/dist/hooks.js +56 -0
- package/dist/hooks.test.d.ts +4 -0
- package/dist/hooks.test.js +92 -0
- package/dist/interceptor.js +70 -23
- package/dist/main.js +2 -0
- package/dist/output.js +12 -6
- package/dist/pipeline/executor.js +1 -1
- package/dist/pipeline/steps/browser.js +1 -3
- package/dist/pipeline/steps/download.js +42 -26
- package/dist/pipeline/steps/download.test.d.ts +1 -0
- package/dist/pipeline/steps/download.test.js +101 -0
- package/dist/pipeline/steps/fetch.js +40 -22
- package/dist/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/pipeline/steps/fetch.test.js +123 -0
- package/dist/pipeline/steps/transform.js +2 -6
- package/dist/pipeline/template.js +66 -52
- package/dist/pipeline/template.test.js +28 -0
- package/dist/pipeline/transform.test.js +18 -0
- package/dist/plugin.d.ts +40 -1
- package/dist/plugin.js +214 -17
- package/dist/plugin.test.d.ts +1 -1
- package/dist/plugin.test.js +219 -3
- package/dist/record.js +6 -98
- package/dist/registry-api.d.ts +2 -0
- package/dist/registry-api.js +1 -0
- package/dist/registry.d.ts +5 -2
- package/dist/registry.js +1 -2
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +14 -4
- package/dist/snapshotFormatter.d.ts +7 -14
- package/dist/snapshotFormatter.js +38 -78
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +29 -0
- package/dist/validate.js +3 -5
- package/dist/yaml-schema.d.ts +26 -0
- package/dist/yaml-schema.js +5 -0
- package/docs/.vitepress/config.mts +3 -0
- package/docs/adapters/browser/dictionary.md +27 -0
- package/docs/adapters/browser/jd.md +27 -0
- package/docs/adapters/browser/linkedin.md +6 -0
- package/docs/adapters/browser/pixiv.md +92 -0
- package/docs/adapters/browser/web.md +30 -0
- package/docs/adapters/browser/xueqiu.md +27 -9
- package/docs/adapters/index.md +3 -1
- package/docs/comparison.md +125 -0
- package/docs/developer/contributing.md +21 -2
- package/docs/developer/testing.md +14 -8
- package/docs/developer/ts-adapter.md +18 -0
- package/docs/developer/yaml-adapter.md +16 -0
- package/docs/guide/plugins.md +10 -0
- package/docs/zh/guide/plugins.md +10 -0
- package/extension/dist/background.js +519 -444
- package/extension/manifest.json +1 -1
- package/extension/package.json +1 -1
- package/extension/src/background.test.ts +46 -1
- package/extension/src/background.ts +108 -33
- package/extension/src/cdp.ts +9 -9
- package/package.json +3 -2
- package/scripts/check-doc-coverage.sh +2 -0
- package/src/analysis.ts +170 -0
- package/src/browser/cdp.test.ts +66 -0
- package/src/browser/cdp.ts +59 -44
- package/src/browser/dom-snapshot.test.ts +42 -0
- package/src/browser/dom-snapshot.ts +56 -3
- package/src/browser/index.ts +2 -2
- package/src/browser/mcp.ts +2 -4
- package/src/browser/page.ts +34 -37
- package/src/browser/stealth.ts +24 -10
- package/src/browser.test.ts +2 -2
- package/src/build-manifest.test.ts +14 -0
- package/src/build-manifest.ts +13 -31
- package/src/cascade.ts +5 -3
- package/src/cli.ts +66 -34
- package/src/clis/_shared/desktop-commands.ts +121 -0
- package/src/clis/antigravity/serve.ts +6 -3
- package/src/clis/arxiv/search.ts +1 -1
- package/src/clis/bilibili/dynamic.test.ts +79 -0
- package/src/clis/bilibili/favorite.ts +5 -2
- package/src/clis/bilibili/following.ts +3 -2
- package/src/clis/bilibili/subtitle.ts +8 -7
- package/src/clis/bilibili/utils.ts +2 -2
- package/src/clis/boss/batchgreet.ts +1 -1
- package/src/clis/boss/chatlist.ts +1 -1
- package/src/clis/boss/chatmsg.ts +1 -1
- package/src/clis/boss/detail.ts +1 -1
- package/src/clis/boss/exchange.ts +1 -1
- package/src/clis/boss/greet.ts +1 -1
- package/src/clis/boss/invite.ts +1 -1
- package/src/clis/boss/joblist.ts +1 -1
- package/src/clis/boss/mark.ts +4 -3
- package/src/clis/boss/recommend.ts +1 -1
- package/src/clis/boss/resume.ts +1 -1
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +5 -4
- package/src/clis/boss/stats.ts +1 -1
- package/src/clis/chatgpt/ask.ts +5 -0
- package/src/clis/chatgpt/new.ts +7 -2
- package/src/clis/chatgpt/read.ts +7 -2
- package/src/clis/chatgpt/send.ts +3 -2
- package/src/clis/chatgpt/status.ts +6 -1
- package/src/clis/chatwise/ask.ts +7 -2
- package/src/clis/chatwise/export.ts +2 -0
- package/src/clis/chatwise/history.ts +2 -0
- package/src/clis/chatwise/model.ts +7 -3
- package/src/clis/chatwise/new.ts +3 -20
- package/src/clis/chatwise/read.ts +2 -0
- package/src/clis/chatwise/screenshot.ts +3 -32
- package/src/clis/chatwise/send.ts +7 -2
- package/src/clis/chatwise/shared.ts +8 -0
- package/src/clis/chatwise/status.ts +3 -24
- package/src/clis/codex/ask.ts +5 -2
- package/src/clis/codex/dump.ts +2 -27
- package/src/clis/codex/new.ts +2 -28
- package/src/clis/codex/screenshot.ts +2 -32
- package/src/clis/codex/send.ts +5 -4
- package/src/clis/codex/status.ts +2 -24
- package/src/clis/cursor/ask.ts +2 -1
- package/src/clis/cursor/composer.ts +2 -1
- package/src/clis/cursor/dump.ts +2 -27
- package/src/clis/cursor/new.ts +2 -20
- package/src/clis/cursor/read.ts +2 -1
- package/src/clis/cursor/screenshot.ts +1 -36
- package/src/clis/cursor/send.ts +2 -1
- package/src/clis/cursor/status.ts +2 -22
- package/src/clis/dictionary/examples.yaml +25 -0
- package/src/clis/dictionary/search.yaml +27 -0
- package/src/clis/dictionary/synonyms.yaml +25 -0
- package/src/clis/douban/book-hot.ts +1 -1
- package/src/clis/douban/movie-hot.ts +1 -1
- package/src/clis/douban/search.ts +1 -1
- package/src/clis/douban/utils.ts +165 -1
- package/src/clis/doubao/ask.ts +1 -1
- package/src/clis/doubao/new.ts +1 -1
- package/src/clis/doubao/read.ts +1 -1
- package/src/clis/doubao/send.ts +1 -1
- package/src/clis/doubao/status.ts +1 -1
- package/src/clis/doubao-app/ask.ts +1 -1
- package/src/clis/doubao-app/new.ts +1 -1
- package/src/clis/doubao-app/read.ts +1 -1
- package/src/clis/doubao-app/send.ts +1 -1
- package/src/clis/grok/ask.test.ts +25 -0
- package/src/clis/grok/ask.ts +25 -12
- package/src/clis/jd/item.test.ts +35 -0
- package/src/clis/jd/item.ts +101 -0
- package/src/clis/jike/feed.ts +1 -1
- package/src/clis/jike/search.ts +1 -1
- package/src/clis/linkedin/search.ts +5 -4
- package/src/clis/linkedin/timeline.test.ts +99 -0
- package/src/clis/linkedin/timeline.ts +532 -0
- package/src/clis/medium/feed.ts +1 -1
- package/src/clis/medium/search.ts +1 -1
- package/src/clis/medium/user.ts +1 -1
- package/src/clis/medium/{shared.ts → utils.ts} +2 -1
- package/src/clis/pixiv/detail.yaml +49 -0
- package/src/clis/pixiv/download.test.ts +114 -0
- package/src/clis/pixiv/download.ts +91 -0
- package/src/clis/pixiv/illusts.test.ts +115 -0
- package/src/clis/pixiv/illusts.ts +78 -0
- package/src/clis/pixiv/ranking.yaml +53 -0
- package/src/clis/pixiv/search.test.ts +97 -0
- package/src/clis/pixiv/search.ts +53 -0
- package/src/clis/pixiv/test-utils.ts +29 -0
- package/src/clis/pixiv/user.yaml +46 -0
- package/src/clis/pixiv/utils.ts +62 -0
- package/src/clis/reddit/comment.ts +2 -1
- package/src/clis/reddit/read.test.ts +34 -0
- package/src/clis/reddit/read.ts +4 -3
- package/src/clis/reddit/save.ts +2 -1
- package/src/clis/reddit/saved.ts +6 -2
- package/src/clis/reddit/subscribe.ts +2 -1
- package/src/clis/reddit/upvote.ts +2 -1
- package/src/clis/reddit/upvoted.ts +6 -2
- package/src/clis/sinablog/article.ts +1 -1
- package/src/clis/sinablog/hot.ts +1 -1
- package/src/clis/sinablog/user.ts +1 -1
- package/src/clis/substack/feed.ts +1 -1
- package/src/clis/substack/publication.ts +1 -1
- package/src/clis/substack/search.ts +3 -2
- package/src/clis/substack/{shared.ts → utils.ts} +3 -2
- package/src/clis/tiktok/search.yaml +2 -1
- package/src/clis/twitter/accept.ts +2 -1
- package/src/clis/twitter/article.ts +3 -1
- package/src/clis/twitter/block.ts +2 -1
- package/src/clis/twitter/bookmark.ts +2 -1
- package/src/clis/twitter/bookmarks.ts +3 -2
- package/src/clis/twitter/delete.ts +2 -1
- package/src/clis/twitter/follow.ts +2 -1
- package/src/clis/twitter/followers.ts +3 -2
- package/src/clis/twitter/following.ts +3 -2
- package/src/clis/twitter/hide-reply.ts +2 -1
- package/src/clis/twitter/like.ts +2 -1
- package/src/clis/twitter/notifications.ts +2 -1
- package/src/clis/twitter/post.ts +2 -1
- package/src/clis/twitter/profile.ts +4 -2
- package/src/clis/twitter/reply-dm.ts +2 -1
- package/src/clis/twitter/reply.ts +2 -1
- package/src/clis/twitter/search.test.ts +113 -0
- package/src/clis/twitter/search.ts +38 -14
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/timeline.ts +3 -2
- package/src/clis/twitter/trending.ts +3 -2
- package/src/clis/twitter/unblock.ts +2 -1
- package/src/clis/twitter/unbookmark.ts +2 -1
- package/src/clis/twitter/unfollow.ts +2 -1
- package/src/clis/v2ex/daily.ts +3 -2
- package/src/clis/v2ex/me.ts +3 -2
- package/src/clis/v2ex/notifications.ts +3 -4
- package/src/clis/web/read.ts +210 -0
- package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
- package/src/clis/xueqiu/danjuan-utils.ts +176 -0
- package/src/clis/xueqiu/fund-holdings.ts +32 -0
- package/src/clis/xueqiu/fund-snapshot.ts +27 -0
- package/src/clis/youtube/transcript.ts +5 -4
- package/src/clis/youtube/video.ts +3 -2
- package/src/daemon.ts +5 -4
- package/src/discovery.ts +12 -34
- package/src/doctor.ts +3 -2
- package/src/download/index.test.ts +93 -2
- package/src/download/index.ts +44 -23
- package/src/download/media-download.ts +5 -3
- package/src/engine.test.ts +84 -3
- package/src/execution.ts +62 -46
- package/src/explore.ts +21 -90
- package/src/external-clis.yaml +0 -8
- package/src/external.test.ts +9 -0
- package/src/external.ts +12 -10
- package/src/generate.ts +4 -41
- package/src/hooks.test.ts +126 -0
- package/src/hooks.ts +90 -0
- package/src/interceptor.ts +73 -23
- package/src/main.ts +2 -0
- package/src/output.ts +14 -6
- package/src/pipeline/executor.ts +1 -1
- package/src/pipeline/steps/browser.ts +1 -3
- package/src/pipeline/steps/download.test.ts +136 -0
- package/src/pipeline/steps/download.ts +47 -34
- package/src/pipeline/steps/fetch.test.ts +179 -0
- package/src/pipeline/steps/fetch.ts +39 -23
- package/src/pipeline/steps/transform.ts +2 -6
- package/src/pipeline/template.test.ts +28 -0
- package/src/pipeline/template.ts +67 -79
- package/src/pipeline/transform.test.ts +20 -0
- package/src/plugin.test.ts +251 -3
- package/src/plugin.ts +265 -21
- package/src/record.ts +12 -84
- package/src/registry-api.ts +2 -0
- package/src/registry.ts +7 -4
- package/src/runtime.ts +14 -4
- package/src/snapshotFormatter.ts +43 -121
- package/src/utils.ts +39 -0
- package/src/validate.ts +3 -5
- package/src/yaml-schema.ts +28 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
- package/tests/e2e/plugin-management.test.ts +137 -0
- package/tests/e2e/public-commands.test.ts +34 -1
- package/vitest.config.ts +19 -1
- package/.github/workflows/pkg-pr-new.yml +0 -30
- package/dist/clis/douban/shared.d.ts +0 -4
- package/dist/clis/douban/shared.js +0 -155
- package/src/clis/douban/shared.ts +0 -165
- /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/boss/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
- /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/jike/{shared.js → utils.js} +0 -0
- /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
- /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
- /package/src/clis/boss/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
- /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
- /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
package/src/browser/cdp.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { WebSocket, type RawData } from 'ws';
|
|
12
|
+
import { request as httpRequest } from 'node:http';
|
|
13
|
+
import { request as httpsRequest } from 'node:https';
|
|
12
14
|
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
|
|
13
15
|
import { wrapForEval } from './utils.js';
|
|
14
16
|
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
networkRequestsJs,
|
|
24
26
|
waitForDomStableJs,
|
|
25
27
|
} from './dom-helpers.js';
|
|
28
|
+
import { isRecord, saveBase64ToFile } from '../utils.js';
|
|
26
29
|
|
|
27
30
|
export interface CDPTarget {
|
|
28
31
|
type?: string;
|
|
@@ -42,7 +45,7 @@ interface RuntimeEvaluateResult {
|
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
const CDP_SEND_TIMEOUT = 30_000;
|
|
48
|
+
const CDP_SEND_TIMEOUT = 30_000;
|
|
46
49
|
|
|
47
50
|
export class CDPBridge {
|
|
48
51
|
private _ws: WebSocket | null = null;
|
|
@@ -51,15 +54,14 @@ export class CDPBridge {
|
|
|
51
54
|
private _eventListeners = new Map<string, Set<(params: unknown) => void>>();
|
|
52
55
|
|
|
53
56
|
async connect(opts?: { timeout?: number; workspace?: string }): Promise<IPage> {
|
|
57
|
+
if (this._ws) throw new Error('CDPBridge is already connected. Call close() before reconnecting.');
|
|
58
|
+
|
|
54
59
|
const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
55
60
|
if (!endpoint) throw new Error('OPENCLI_CDP_ENDPOINT is not set');
|
|
56
61
|
|
|
57
|
-
// If it's a direct ws:// URL, use it. Otherwise, fetch the /json endpoint to find a page.
|
|
58
62
|
let wsUrl = endpoint;
|
|
59
63
|
if (endpoint.startsWith('http')) {
|
|
60
|
-
const
|
|
61
|
-
if (!res.ok) throw new Error(`Failed to fetch CDP targets: ${res.statusText}`);
|
|
62
|
-
const targets = await res.json() as CDPTarget[];
|
|
64
|
+
const targets = await fetchJsonDirect(`${endpoint.replace(/\/$/, '')}/json`) as CDPTarget[];
|
|
63
65
|
const target = selectCDPTarget(targets);
|
|
64
66
|
if (!target || !target.webSocketDebuggerUrl) {
|
|
65
67
|
throw new Error('No inspectable targets found at CDP endpoint');
|
|
@@ -69,19 +71,16 @@ export class CDPBridge {
|
|
|
69
71
|
|
|
70
72
|
return new Promise((resolve, reject) => {
|
|
71
73
|
const ws = new WebSocket(wsUrl);
|
|
72
|
-
const timeoutMs = (opts?.timeout ?? 10) * 1000;
|
|
74
|
+
const timeoutMs = (opts?.timeout ?? 10) * 1000;
|
|
73
75
|
const timeout = setTimeout(() => reject(new Error('CDP connect timeout')), timeoutMs);
|
|
74
76
|
|
|
75
77
|
ws.on('open', async () => {
|
|
76
78
|
clearTimeout(timeout);
|
|
77
79
|
this._ws = ws;
|
|
78
|
-
// Register stealth script to run before any page JS on every navigation.
|
|
79
80
|
try {
|
|
80
81
|
await this.send('Page.enable');
|
|
81
82
|
await this.send('Page.addScriptToEvaluateOnNewDocument', { source: generateStealthJs() });
|
|
82
|
-
} catch {
|
|
83
|
-
// Non-fatal: stealth is best-effort
|
|
84
|
-
}
|
|
83
|
+
} catch {}
|
|
85
84
|
resolve(new CDPPage(this));
|
|
86
85
|
});
|
|
87
86
|
|
|
@@ -93,7 +92,6 @@ export class CDPBridge {
|
|
|
93
92
|
ws.on('message', (data: RawData) => {
|
|
94
93
|
try {
|
|
95
94
|
const msg = JSON.parse(data.toString());
|
|
96
|
-
// Handle command responses
|
|
97
95
|
if (msg.id && this._pending.has(msg.id)) {
|
|
98
96
|
const entry = this._pending.get(msg.id)!;
|
|
99
97
|
clearTimeout(entry.timer);
|
|
@@ -104,16 +102,13 @@ export class CDPBridge {
|
|
|
104
102
|
entry.resolve(msg.result);
|
|
105
103
|
}
|
|
106
104
|
}
|
|
107
|
-
// Handle CDP events
|
|
108
105
|
if (msg.method) {
|
|
109
106
|
const listeners = this._eventListeners.get(msg.method);
|
|
110
107
|
if (listeners) {
|
|
111
108
|
for (const fn of listeners) fn(msg.params);
|
|
112
109
|
}
|
|
113
110
|
}
|
|
114
|
-
} catch {
|
|
115
|
-
// ignore parsing errors
|
|
116
|
-
}
|
|
111
|
+
} catch {}
|
|
117
112
|
});
|
|
118
113
|
});
|
|
119
114
|
}
|
|
@@ -131,7 +126,6 @@ export class CDPBridge {
|
|
|
131
126
|
this._eventListeners.clear();
|
|
132
127
|
}
|
|
133
128
|
|
|
134
|
-
/** Send a CDP command with timeout guard (P0 fix #4) */
|
|
135
129
|
async send(method: string, params: Record<string, unknown> = {}, timeoutMs: number = CDP_SEND_TIMEOUT): Promise<unknown> {
|
|
136
130
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
137
131
|
throw new Error('CDP connection is not open');
|
|
@@ -147,19 +141,19 @@ export class CDPBridge {
|
|
|
147
141
|
});
|
|
148
142
|
}
|
|
149
143
|
|
|
150
|
-
/** Listen for a CDP event */
|
|
151
144
|
on(event: string, handler: (params: unknown) => void): void {
|
|
152
145
|
let set = this._eventListeners.get(event);
|
|
153
|
-
if (!set) {
|
|
146
|
+
if (!set) {
|
|
147
|
+
set = new Set();
|
|
148
|
+
this._eventListeners.set(event, set);
|
|
149
|
+
}
|
|
154
150
|
set.add(handler);
|
|
155
151
|
}
|
|
156
152
|
|
|
157
|
-
/** Remove a CDP event listener */
|
|
158
153
|
off(event: string, handler: (params: unknown) => void): void {
|
|
159
154
|
this._eventListeners.get(event)?.delete(handler);
|
|
160
155
|
}
|
|
161
156
|
|
|
162
|
-
/** Wait for a CDP event to fire (one-shot) */
|
|
163
157
|
waitForEvent(event: string, timeoutMs: number = 15_000): Promise<unknown> {
|
|
164
158
|
return new Promise((resolve, reject) => {
|
|
165
159
|
const timer = setTimeout(() => {
|
|
@@ -177,17 +171,17 @@ export class CDPBridge {
|
|
|
177
171
|
}
|
|
178
172
|
|
|
179
173
|
class CDPPage implements IPage {
|
|
174
|
+
private _pageEnabled = false;
|
|
180
175
|
constructor(private bridge: CDPBridge) {}
|
|
181
176
|
|
|
182
|
-
/** Navigate with proper load event waiting (P1 fix #3) */
|
|
183
177
|
async goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise<void> {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.
|
|
178
|
+
if (!this._pageEnabled) {
|
|
179
|
+
await this.bridge.send('Page.enable');
|
|
180
|
+
this._pageEnabled = true;
|
|
181
|
+
}
|
|
182
|
+
const loadPromise = this.bridge.waitForEvent('Page.loadEventFired', 30_000).catch(() => {});
|
|
187
183
|
await this.bridge.send('Page.navigate', { url });
|
|
188
184
|
await loadPromise;
|
|
189
|
-
// Smart settle: use DOM stability detection instead of fixed sleep.
|
|
190
|
-
// settleMs is now a timeout cap (default 1000ms), not a fixed wait.
|
|
191
185
|
if (options?.waitUntil !== 'none') {
|
|
192
186
|
const maxMs = options?.settleMs ?? 1000;
|
|
193
187
|
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
|
|
@@ -199,7 +193,7 @@ class CDPPage implements IPage {
|
|
|
199
193
|
const result = await this.bridge.send('Runtime.evaluate', {
|
|
200
194
|
expression,
|
|
201
195
|
returnByValue: true,
|
|
202
|
-
awaitPromise: true
|
|
196
|
+
awaitPromise: true,
|
|
203
197
|
}) as RuntimeEvaluateResult;
|
|
204
198
|
if (result.exceptionDetails) {
|
|
205
199
|
throw new Error('Evaluate error: ' + (result.exceptionDetails.exception?.description || 'Unknown exception'));
|
|
@@ -212,7 +206,7 @@ class CDPPage implements IPage {
|
|
|
212
206
|
const cookies = isRecord(result) && Array.isArray(result.cookies) ? result.cookies : [];
|
|
213
207
|
const domain = opts.domain;
|
|
214
208
|
return domain
|
|
215
|
-
? cookies.filter((cookie): cookie is BrowserCookie => isCookie(cookie) && cookie.domain
|
|
209
|
+
? cookies.filter((cookie): cookie is BrowserCookie => isCookie(cookie) && matchesCookieDomain(cookie.domain, domain))
|
|
216
210
|
: cookies;
|
|
217
211
|
}
|
|
218
212
|
|
|
@@ -228,8 +222,6 @@ class CDPPage implements IPage {
|
|
|
228
222
|
return this.evaluate(snapshotJs);
|
|
229
223
|
}
|
|
230
224
|
|
|
231
|
-
// ── Shared DOM operations (P1 fix #5 — using dom-helpers.ts) ──
|
|
232
|
-
|
|
233
225
|
async click(ref: string): Promise<void> {
|
|
234
226
|
await this.evaluate(clickJs(ref));
|
|
235
227
|
}
|
|
@@ -252,12 +244,12 @@ class CDPPage implements IPage {
|
|
|
252
244
|
|
|
253
245
|
async wait(options: number | WaitOptions): Promise<void> {
|
|
254
246
|
if (typeof options === 'number') {
|
|
255
|
-
await new Promise(resolve => setTimeout(resolve, options * 1000));
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, options * 1000));
|
|
256
248
|
return;
|
|
257
249
|
}
|
|
258
250
|
if (typeof options.time === 'number') {
|
|
259
251
|
const waitTime = options.time;
|
|
260
|
-
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime * 1000));
|
|
261
253
|
return;
|
|
262
254
|
}
|
|
263
255
|
if (options.text) {
|
|
@@ -266,8 +258,6 @@ class CDPPage implements IPage {
|
|
|
266
258
|
}
|
|
267
259
|
}
|
|
268
260
|
|
|
269
|
-
// ── Implemented methods (P1 fix #2) ──
|
|
270
|
-
|
|
271
261
|
async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
|
|
272
262
|
await this.evaluate(scrollJs(direction, amount));
|
|
273
263
|
}
|
|
@@ -286,11 +276,7 @@ class CDPPage implements IPage {
|
|
|
286
276
|
});
|
|
287
277
|
const base64 = isRecord(result) && typeof result.data === 'string' ? result.data : '';
|
|
288
278
|
if (options.path) {
|
|
289
|
-
|
|
290
|
-
const path = await import('node:path');
|
|
291
|
-
const dir = path.dirname(options.path);
|
|
292
|
-
await fs.promises.mkdir(dir, { recursive: true });
|
|
293
|
-
await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
|
|
279
|
+
await saveBase64ToFile(base64, options.path);
|
|
294
280
|
}
|
|
295
281
|
return base64;
|
|
296
282
|
}
|
|
@@ -335,10 +321,6 @@ class CDPPage implements IPage {
|
|
|
335
321
|
}
|
|
336
322
|
}
|
|
337
323
|
|
|
338
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
339
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
324
|
function isCookie(value: unknown): value is BrowserCookie {
|
|
343
325
|
return isRecord(value)
|
|
344
326
|
&& typeof value.name === 'string'
|
|
@@ -346,7 +328,12 @@ function isCookie(value: unknown): value is BrowserCookie {
|
|
|
346
328
|
&& typeof value.domain === 'string';
|
|
347
329
|
}
|
|
348
330
|
|
|
349
|
-
|
|
331
|
+
function matchesCookieDomain(cookieDomain: string, targetDomain: string): boolean {
|
|
332
|
+
const normalizedCookieDomain = cookieDomain.replace(/^\./, '').toLowerCase();
|
|
333
|
+
const normalizedTargetDomain = targetDomain.replace(/^\./, '').toLowerCase();
|
|
334
|
+
return normalizedTargetDomain === normalizedCookieDomain
|
|
335
|
+
|| normalizedTargetDomain.endsWith(`.${normalizedCookieDomain}`);
|
|
336
|
+
}
|
|
350
337
|
|
|
351
338
|
function selectCDPTarget(targets: CDPTarget[]): CDPTarget | undefined {
|
|
352
339
|
const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET);
|
|
@@ -420,3 +407,31 @@ export const __test__ = {
|
|
|
420
407
|
selectCDPTarget,
|
|
421
408
|
scoreCDPTarget,
|
|
422
409
|
};
|
|
410
|
+
|
|
411
|
+
function fetchJsonDirect(url: string): Promise<unknown> {
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
const parsed = new URL(url);
|
|
414
|
+
const request = (parsed.protocol === 'https:' ? httpsRequest : httpRequest)(parsed, (res) => {
|
|
415
|
+
const statusCode = res.statusCode ?? 0;
|
|
416
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
417
|
+
res.resume();
|
|
418
|
+
reject(new Error(`Failed to fetch CDP targets: HTTP ${statusCode}`));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const chunks: Buffer[] = [];
|
|
423
|
+
res.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
424
|
+
res.on('end', () => {
|
|
425
|
+
try {
|
|
426
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
427
|
+
} catch (error) {
|
|
428
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
request.on('error', reject);
|
|
434
|
+
request.setTimeout(10_000, () => request.destroy(new Error('Timed out fetching CDP targets')));
|
|
435
|
+
request.end();
|
|
436
|
+
});
|
|
437
|
+
}
|
|
@@ -247,3 +247,45 @@ describe('getFormStateJs', () => {
|
|
|
247
247
|
expect(js).toContain('data-opencli-ref');
|
|
248
248
|
});
|
|
249
249
|
});
|
|
250
|
+
|
|
251
|
+
describe('Search Element Detection', () => {
|
|
252
|
+
it('includes SEARCH_INDICATORS set', () => {
|
|
253
|
+
const js = generateSnapshotJs();
|
|
254
|
+
expect(js).toContain('SEARCH_INDICATORS');
|
|
255
|
+
expect(js).toContain('search');
|
|
256
|
+
expect(js).toContain('magnify');
|
|
257
|
+
expect(js).toContain('glass');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('includes hasFormControlDescendant function', () => {
|
|
261
|
+
const js = generateSnapshotJs();
|
|
262
|
+
expect(js).toContain('hasFormControlDescendant');
|
|
263
|
+
expect(js).toContain('input');
|
|
264
|
+
expect(js).toContain('select');
|
|
265
|
+
expect(js).toContain('textarea');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('includes isSearchElement function', () => {
|
|
269
|
+
const js = generateSnapshotJs();
|
|
270
|
+
expect(js).toContain('isSearchElement');
|
|
271
|
+
expect(js).toContain('className');
|
|
272
|
+
expect(js).toContain('data-');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('checks label wrapper detection in isInteractive', () => {
|
|
276
|
+
const js = generateSnapshotJs();
|
|
277
|
+
// Label elements without "for" attribute should check for form control descendants
|
|
278
|
+
expect(js).toContain('hasFormControlDescendant(el, 2)');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('checks span wrapper detection in isInteractive', () => {
|
|
282
|
+
const js = generateSnapshotJs();
|
|
283
|
+
// Span elements should check for form control descendants
|
|
284
|
+
expect(js).toContain("tag === 'span'");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('integrates search element detection into isInteractive', () => {
|
|
288
|
+
const js = generateSnapshotJs();
|
|
289
|
+
expect(js).toContain('isSearchElement(el)');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
// ─── Types ───────────────────────────────────────────────────────────
|
|
28
28
|
|
|
29
|
-
export interface
|
|
29
|
+
export interface DomSnapshotOptions {
|
|
30
30
|
/** Extra pixels beyond viewport to include (default 800) */
|
|
31
31
|
viewportExpand?: number;
|
|
32
32
|
/** Maximum DOM depth to traverse (default 50) */
|
|
@@ -175,7 +175,7 @@ export function getFormStateJs(): string {
|
|
|
175
175
|
* - `|iframe|` — iframe content
|
|
176
176
|
* - `|table|` — markdown table rendering
|
|
177
177
|
*/
|
|
178
|
-
export function generateSnapshotJs(opts:
|
|
178
|
+
export function generateSnapshotJs(opts: DomSnapshotOptions = {}): string {
|
|
179
179
|
const viewportExpand = opts.viewportExpand ?? 800;
|
|
180
180
|
const maxDepth = Math.max(1, Math.min(opts.maxDepth ?? 50, 200));
|
|
181
181
|
const interactiveOnly = opts.interactiveOnly ?? false;
|
|
@@ -271,6 +271,13 @@ export function generateSnapshotJs(opts: SnapshotOptions = {}): string {
|
|
|
271
271
|
|
|
272
272
|
const AD_SELECTOR_RE = /\\b(ad[_-]?(?:banner|container|wrapper|slot|unit|block|frame|leaderboard|sidebar)|google[_-]?ad|sponsored|adsbygoogle|banner[_-]?ad)\\b/i;
|
|
273
273
|
|
|
274
|
+
// Search element indicators for heuristic detection
|
|
275
|
+
const SEARCH_INDICATORS = new Set([
|
|
276
|
+
'search', 'magnify', 'glass', 'lookup', 'find', 'query',
|
|
277
|
+
'search-icon', 'search-btn', 'search-button', 'searchbox',
|
|
278
|
+
'fa-search', 'icon-search', 'btn-search',
|
|
279
|
+
]);
|
|
280
|
+
|
|
274
281
|
// ── Viewport & Layout Helpers ──────────────────────────────────────
|
|
275
282
|
|
|
276
283
|
const vw = window.innerWidth;
|
|
@@ -339,19 +346,65 @@ export function generateSnapshotJs(opts: SnapshotOptions = {}): string {
|
|
|
339
346
|
|
|
340
347
|
// ── Interactivity Detection ────────────────────────────────────────
|
|
341
348
|
|
|
349
|
+
// Check if element contains a form control within limited depth (handles label/span wrappers)
|
|
350
|
+
function hasFormControlDescendant(el, maxDepth = 2) {
|
|
351
|
+
if (maxDepth <= 0) return false;
|
|
352
|
+
for (const child of el.children || []) {
|
|
353
|
+
const tag = child.tagName?.toLowerCase();
|
|
354
|
+
if (tag === 'input' || tag === 'select' || tag === 'textarea') return true;
|
|
355
|
+
if (hasFormControlDescendant(child, maxDepth - 1)) return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
342
360
|
function isInteractive(el) {
|
|
343
361
|
const tag = el.tagName.toLowerCase();
|
|
344
362
|
if (INTERACTIVE_TAGS.has(tag)) {
|
|
345
|
-
|
|
363
|
+
// Skip labels that proxy via "for" to avoid double-activating external inputs
|
|
364
|
+
if (tag === 'label') {
|
|
365
|
+
if (el.hasAttribute('for')) return false;
|
|
366
|
+
// Detect labels that wrap form controls up to two levels deep (label > span > input)
|
|
367
|
+
if (hasFormControlDescendant(el, 2)) return true;
|
|
368
|
+
}
|
|
346
369
|
if (el.disabled && (tag === 'button' || tag === 'input')) return false;
|
|
347
370
|
return true;
|
|
348
371
|
}
|
|
372
|
+
// Span wrappers for UI components - check if they contain form controls
|
|
373
|
+
if (tag === 'span') {
|
|
374
|
+
if (hasFormControlDescendant(el, 2)) return true;
|
|
375
|
+
}
|
|
349
376
|
const role = el.getAttribute('role');
|
|
350
377
|
if (role && INTERACTIVE_ROLES.has(role)) return true;
|
|
351
378
|
if (el.hasAttribute('onclick') || el.hasAttribute('onmousedown') || el.hasAttribute('ontouchstart')) return true;
|
|
352
379
|
if (el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') return true;
|
|
353
380
|
try { if (window.getComputedStyle(el).cursor === 'pointer') return true; } catch {}
|
|
354
381
|
if (el.isContentEditable && el.getAttribute('contenteditable') !== 'false') return true;
|
|
382
|
+
// Search element heuristic detection
|
|
383
|
+
if (isSearchElement(el)) return true;
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isSearchElement(el) {
|
|
388
|
+
// Check class names for search indicators
|
|
389
|
+
const className = el.className?.toLowerCase() || '';
|
|
390
|
+
const classes = className.split(/\\s+/).filter(Boolean);
|
|
391
|
+
for (const cls of classes) {
|
|
392
|
+
const cleaned = cls.replace(/[^a-z0-9-]/g, '');
|
|
393
|
+
if (SEARCH_INDICATORS.has(cleaned)) return true;
|
|
394
|
+
}
|
|
395
|
+
// Check id for search indicators
|
|
396
|
+
const id = el.id?.toLowerCase() || '';
|
|
397
|
+
const cleanedId = id.replace(/[^a-z0-9-]/g, '');
|
|
398
|
+
if (SEARCH_INDICATORS.has(cleanedId)) return true;
|
|
399
|
+
// Check data-* attributes for search functionality
|
|
400
|
+
for (const attr of el.attributes || []) {
|
|
401
|
+
if (attr.name.startsWith('data-')) {
|
|
402
|
+
const value = attr.value.toLowerCase();
|
|
403
|
+
for (const kw of SEARCH_INDICATORS) {
|
|
404
|
+
if (value.includes(kw)) return true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
355
408
|
return false;
|
|
356
409
|
}
|
|
357
410
|
|
package/src/browser/index.ts
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export { Page } from './page.js';
|
|
9
|
-
export { BrowserBridge
|
|
9
|
+
export { BrowserBridge } from './mcp.js';
|
|
10
10
|
export { CDPBridge } from './cdp.js';
|
|
11
11
|
export { isDaemonRunning } from './daemon-client.js';
|
|
12
12
|
export { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
13
13
|
export { generateStealthJs } from './stealth.js';
|
|
14
|
-
export type {
|
|
14
|
+
export type { DomSnapshotOptions } from './dom-snapshot.js';
|
|
15
15
|
|
|
16
16
|
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
17
17
|
import { __test__ as cdpTest } from './cdp.js';
|
package/src/browser/mcp.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as fs from 'node:fs';
|
|
|
9
9
|
import type { IPage } from '../types.js';
|
|
10
10
|
import { Page } from './page.js';
|
|
11
11
|
import { isDaemonRunning, isExtensionConnected } from './daemon-client.js';
|
|
12
|
+
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
12
13
|
|
|
13
14
|
const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension
|
|
14
15
|
|
|
@@ -112,10 +113,7 @@ export class BrowserBridge {
|
|
|
112
113
|
throw new Error(
|
|
113
114
|
'Failed to start opencli daemon. Try running manually:\n' +
|
|
114
115
|
` node ${daemonPath}\n` +
|
|
115
|
-
|
|
116
|
+
`Make sure port ${DEFAULT_DAEMON_PORT} is available.`,
|
|
116
117
|
);
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
/** @deprecated Use BrowserBridge instead */
|
|
121
|
-
export const PlaywrightMCP = BrowserBridge;
|
package/src/browser/page.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { formatSnapshot } from '../snapshotFormatter.js';
|
|
|
14
14
|
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
|
|
15
15
|
import { sendCommand } from './daemon-client.js';
|
|
16
16
|
import { wrapForEval } from './utils.js';
|
|
17
|
+
import { saveBase64ToFile } from '../utils.js';
|
|
17
18
|
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
18
19
|
import { generateStealthJs } from './stealth.js';
|
|
19
20
|
import {
|
|
@@ -36,20 +37,23 @@ export class Page implements IPage {
|
|
|
36
37
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
37
38
|
private _tabId: number | undefined;
|
|
38
39
|
|
|
39
|
-
/** Helper: spread
|
|
40
|
-
private
|
|
41
|
-
return
|
|
40
|
+
/** Helper: spread workspace into command params */
|
|
41
|
+
private _wsOpt(): { workspace: string } {
|
|
42
|
+
return { workspace: this.workspace };
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
/** Helper: spread workspace + tabId into command params */
|
|
46
|
+
private _cmdOpts(): Record<string, unknown> {
|
|
47
|
+
return {
|
|
48
|
+
workspace: this.workspace,
|
|
49
|
+
...(this._tabId !== undefined && { tabId: this._tabId }),
|
|
50
|
+
};
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
async goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise<void> {
|
|
49
54
|
const result = await sendCommand('navigate', {
|
|
50
55
|
url,
|
|
51
|
-
...this.
|
|
52
|
-
...this._tabOpt(),
|
|
56
|
+
...this._cmdOpts(),
|
|
53
57
|
}) as { tabId?: number };
|
|
54
58
|
// Remember the tabId for subsequent exec calls
|
|
55
59
|
if (result?.tabId) {
|
|
@@ -59,8 +63,7 @@ export class Page implements IPage {
|
|
|
59
63
|
try {
|
|
60
64
|
await sendCommand('exec', {
|
|
61
65
|
code: generateStealthJs(),
|
|
62
|
-
...this.
|
|
63
|
-
...this._tabOpt(),
|
|
66
|
+
...this._cmdOpts(),
|
|
64
67
|
});
|
|
65
68
|
} catch {
|
|
66
69
|
// Non-fatal: stealth is best-effort
|
|
@@ -71,8 +74,7 @@ export class Page implements IPage {
|
|
|
71
74
|
const maxMs = options?.settleMs ?? 1000;
|
|
72
75
|
await sendCommand('exec', {
|
|
73
76
|
code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
|
|
74
|
-
...this.
|
|
75
|
-
...this._tabOpt(),
|
|
77
|
+
...this._cmdOpts(),
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -80,7 +82,7 @@ export class Page implements IPage {
|
|
|
80
82
|
/** Close the automation window in the extension */
|
|
81
83
|
async closeWindow(): Promise<void> {
|
|
82
84
|
try {
|
|
83
|
-
await sendCommand('close-window', { ...this.
|
|
85
|
+
await sendCommand('close-window', { ...this._wsOpt() });
|
|
84
86
|
} catch {
|
|
85
87
|
// Window may already be closed or daemon may be down
|
|
86
88
|
}
|
|
@@ -88,11 +90,11 @@ export class Page implements IPage {
|
|
|
88
90
|
|
|
89
91
|
async evaluate(js: string): Promise<unknown> {
|
|
90
92
|
const code = wrapForEval(js);
|
|
91
|
-
return sendCommand('exec', { code, ...this.
|
|
93
|
+
return sendCommand('exec', { code, ...this._cmdOpts() });
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
async getCookies(opts: { domain?: string; url?: string } = {}): Promise<BrowserCookie[]> {
|
|
95
|
-
const result = await sendCommand('cookies', { ...this.
|
|
97
|
+
const result = await sendCommand('cookies', { ...this._wsOpt(), ...opts });
|
|
96
98
|
return Array.isArray(result) ? result : [];
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -108,7 +110,7 @@ export class Page implements IPage {
|
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
try {
|
|
111
|
-
const result = await sendCommand('exec', { code: snapshotJs, ...this.
|
|
113
|
+
const result = await sendCommand('exec', { code: snapshotJs, ...this._cmdOpts() });
|
|
112
114
|
// The advanced engine already produces a clean, pruned, LLM-friendly output.
|
|
113
115
|
// Do NOT pass through formatSnapshot — its format is incompatible.
|
|
114
116
|
return result;
|
|
@@ -148,7 +150,7 @@ export class Page implements IPage {
|
|
|
148
150
|
return buildTree(document.body, 0);
|
|
149
151
|
})()
|
|
150
152
|
`;
|
|
151
|
-
const raw = await sendCommand('exec', { code, ...this.
|
|
153
|
+
const raw = await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
152
154
|
if (opts.raw) return raw;
|
|
153
155
|
if (typeof raw === 'string') return formatSnapshot(raw, opts);
|
|
154
156
|
return raw;
|
|
@@ -156,27 +158,27 @@ export class Page implements IPage {
|
|
|
156
158
|
|
|
157
159
|
async click(ref: string): Promise<void> {
|
|
158
160
|
const code = clickJs(ref);
|
|
159
|
-
await sendCommand('exec', { code, ...this.
|
|
161
|
+
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
async typeText(ref: string, text: string): Promise<void> {
|
|
163
165
|
const code = typeTextJs(ref, text);
|
|
164
|
-
await sendCommand('exec', { code, ...this.
|
|
166
|
+
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
async pressKey(key: string): Promise<void> {
|
|
168
170
|
const code = pressKeyJs(key);
|
|
169
|
-
await sendCommand('exec', { code, ...this.
|
|
171
|
+
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
async scrollTo(ref: string): Promise<unknown> {
|
|
173
175
|
const code = scrollToRefJs(ref);
|
|
174
|
-
return sendCommand('exec', { code, ...this.
|
|
176
|
+
return sendCommand('exec', { code, ...this._cmdOpts() });
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
async getFormState(): Promise<Record<string, unknown>> {
|
|
178
180
|
const code = getFormStateJs();
|
|
179
|
-
return (await sendCommand('exec', { code, ...this.
|
|
181
|
+
return (await sendCommand('exec', { code, ...this._cmdOpts() })) as Record<string, unknown>;
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
async wait(options: number | WaitOptions): Promise<void> {
|
|
@@ -184,42 +186,42 @@ export class Page implements IPage {
|
|
|
184
186
|
await new Promise(resolve => setTimeout(resolve, options * 1000));
|
|
185
187
|
return;
|
|
186
188
|
}
|
|
187
|
-
if (options.time) {
|
|
189
|
+
if (typeof options.time === 'number') {
|
|
188
190
|
await new Promise(resolve => setTimeout(resolve, options.time! * 1000));
|
|
189
191
|
return;
|
|
190
192
|
}
|
|
191
193
|
if (options.text) {
|
|
192
194
|
const timeout = (options.timeout ?? 30) * 1000;
|
|
193
195
|
const code = waitForTextJs(options.text, timeout);
|
|
194
|
-
await sendCommand('exec', { code, ...this.
|
|
196
|
+
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
async tabs(): Promise<unknown[]> {
|
|
199
|
-
const result = await sendCommand('tabs', { op: 'list', ...this.
|
|
201
|
+
const result = await sendCommand('tabs', { op: 'list', ...this._wsOpt() });
|
|
200
202
|
return Array.isArray(result) ? result : [];
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
async closeTab(index?: number): Promise<void> {
|
|
204
|
-
await sendCommand('tabs', { op: 'close', ...this.
|
|
206
|
+
await sendCommand('tabs', { op: 'close', ...this._wsOpt(), ...(index !== undefined ? { index } : {}) });
|
|
205
207
|
// Invalidate cached tabId — the closed tab might have been our active one.
|
|
206
208
|
// We can't know for sure (close-by-index doesn't return tabId), so reset.
|
|
207
209
|
this._tabId = undefined;
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
async newTab(): Promise<void> {
|
|
211
|
-
const result = await sendCommand('tabs', { op: 'new', ...this.
|
|
213
|
+
const result = await sendCommand('tabs', { op: 'new', ...this._wsOpt() }) as { tabId?: number };
|
|
212
214
|
if (result?.tabId) this._tabId = result.tabId;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
async selectTab(index: number): Promise<void> {
|
|
216
|
-
const result = await sendCommand('tabs', { op: 'select', index, ...this.
|
|
218
|
+
const result = await sendCommand('tabs', { op: 'select', index, ...this._wsOpt() }) as { selected?: number };
|
|
217
219
|
if (result?.selected) this._tabId = result.selected;
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
async networkRequests(includeStatic: boolean = false): Promise<unknown[]> {
|
|
221
223
|
const code = networkRequestsJs(includeStatic);
|
|
222
|
-
const result = await sendCommand('exec', { code, ...this.
|
|
224
|
+
const result = await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
223
225
|
return Array.isArray(result) ? result : [];
|
|
224
226
|
}
|
|
225
227
|
|
|
@@ -241,19 +243,14 @@ export class Page implements IPage {
|
|
|
241
243
|
*/
|
|
242
244
|
async screenshot(options: ScreenshotOptions = {}): Promise<string> {
|
|
243
245
|
const base64 = await sendCommand('screenshot', {
|
|
244
|
-
...this.
|
|
246
|
+
...this._cmdOpts(),
|
|
245
247
|
format: options.format,
|
|
246
248
|
quality: options.quality,
|
|
247
249
|
fullPage: options.fullPage,
|
|
248
|
-
...this._tabOpt(),
|
|
249
250
|
}) as string;
|
|
250
251
|
|
|
251
252
|
if (options.path) {
|
|
252
|
-
|
|
253
|
-
const path = await import('node:path');
|
|
254
|
-
const dir = path.dirname(options.path);
|
|
255
|
-
await fs.promises.mkdir(dir, { recursive: true });
|
|
256
|
-
await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
|
|
253
|
+
await saveBase64ToFile(base64, options.path);
|
|
257
254
|
}
|
|
258
255
|
|
|
259
256
|
return base64;
|
|
@@ -261,14 +258,14 @@ export class Page implements IPage {
|
|
|
261
258
|
|
|
262
259
|
async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
|
|
263
260
|
const code = scrollJs(direction, amount);
|
|
264
|
-
await sendCommand('exec', { code, ...this.
|
|
261
|
+
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
265
262
|
}
|
|
266
263
|
|
|
267
264
|
async autoScroll(options: { times?: number; delayMs?: number } = {}): Promise<void> {
|
|
268
265
|
const times = options.times ?? 3;
|
|
269
266
|
const delayMs = options.delayMs ?? 2000;
|
|
270
267
|
const code = autoScrollJs(times, delayMs);
|
|
271
|
-
await sendCommand('exec', { code, ...this.
|
|
268
|
+
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
272
269
|
}
|
|
273
270
|
|
|
274
271
|
async installInterceptor(pattern: string): Promise<void> {
|