@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/extension/manifest.json
CHANGED
package/extension/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
type Listener<T extends (...args: any[]) => void> = { addListener: (fn: T) => void };
|
|
4
4
|
|
|
@@ -96,9 +96,15 @@ function createChromeMock() {
|
|
|
96
96
|
describe('background tab isolation', () => {
|
|
97
97
|
beforeEach(() => {
|
|
98
98
|
vi.resetModules();
|
|
99
|
+
vi.useRealTimers();
|
|
99
100
|
vi.stubGlobal('WebSocket', MockWebSocket);
|
|
100
101
|
});
|
|
101
102
|
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
vi.useRealTimers();
|
|
105
|
+
vi.unstubAllGlobals();
|
|
106
|
+
});
|
|
107
|
+
|
|
102
108
|
it('lists only automation-window web tabs', async () => {
|
|
103
109
|
const { chrome } = createChromeMock();
|
|
104
110
|
vi.stubGlobal('chrome', chrome);
|
|
@@ -133,6 +139,45 @@ describe('background tab isolation', () => {
|
|
|
133
139
|
expect(create).toHaveBeenCalledWith({ windowId: 1, url: 'https://new.example', active: true });
|
|
134
140
|
});
|
|
135
141
|
|
|
142
|
+
it('treats normalized same-url navigate as already complete', async () => {
|
|
143
|
+
const { chrome, tabs, update } = createChromeMock();
|
|
144
|
+
tabs[0].url = 'https://www.bilibili.com/';
|
|
145
|
+
tabs[0].title = 'bilibili';
|
|
146
|
+
tabs[0].status = 'complete';
|
|
147
|
+
vi.stubGlobal('chrome', chrome);
|
|
148
|
+
|
|
149
|
+
const mod = await import('./background');
|
|
150
|
+
mod.__test__.setAutomationWindowId('site:bilibili', 1);
|
|
151
|
+
|
|
152
|
+
const result = await mod.__test__.handleNavigate(
|
|
153
|
+
{ id: 'same-url', action: 'navigate', url: 'https://www.bilibili.com', workspace: 'site:bilibili' },
|
|
154
|
+
'site:bilibili',
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(result).toEqual({
|
|
158
|
+
id: 'same-url',
|
|
159
|
+
ok: true,
|
|
160
|
+
data: {
|
|
161
|
+
title: 'bilibili',
|
|
162
|
+
url: 'https://www.bilibili.com/',
|
|
163
|
+
tabId: 1,
|
|
164
|
+
timedOut: false,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
expect(update).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('keeps hash routes distinct when comparing target URLs', async () => {
|
|
171
|
+
const { chrome } = createChromeMock();
|
|
172
|
+
vi.stubGlobal('chrome', chrome);
|
|
173
|
+
|
|
174
|
+
const mod = await import('./background');
|
|
175
|
+
|
|
176
|
+
expect(mod.__test__.isTargetUrl('https://example.com/', 'https://example.com')).toBe(true);
|
|
177
|
+
expect(mod.__test__.isTargetUrl('https://example.com/#feed', 'https://example.com/#settings')).toBe(false);
|
|
178
|
+
expect(mod.__test__.isTargetUrl('https://example.com/app/', 'https://example.com/app')).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
136
181
|
it('reports sessions per workspace', async () => {
|
|
137
182
|
const { chrome } = createChromeMock();
|
|
138
183
|
vi.stubGlobal('chrome', chrome);
|
|
@@ -138,7 +138,7 @@ async function getAutomationWindow(workspace: string): Promise<number> {
|
|
|
138
138
|
// Create a new window with a data: URI that New Tab Override extensions cannot intercept.
|
|
139
139
|
// Using about:blank would be hijacked by extensions like "New Tab Override".
|
|
140
140
|
const win = await chrome.windows.create({
|
|
141
|
-
url:
|
|
141
|
+
url: BLANK_PAGE,
|
|
142
142
|
focused: false,
|
|
143
143
|
width: 1280,
|
|
144
144
|
height: 900,
|
|
@@ -229,10 +229,37 @@ async function handleCommand(cmd: Command): Promise<Result> {
|
|
|
229
229
|
|
|
230
230
|
// ─── Action handlers ─────────────────────────────────────────────────
|
|
231
231
|
|
|
232
|
-
/**
|
|
232
|
+
/** Internal blank page used when no user URL is provided. */
|
|
233
|
+
const BLANK_PAGE = 'data:text/html,<html></html>';
|
|
234
|
+
|
|
235
|
+
/** Check if a URL can be attached via CDP — only allow http(s) and our internal blank page. */
|
|
233
236
|
function isDebuggableUrl(url?: string): boolean {
|
|
234
237
|
if (!url) return true; // empty/undefined = tab still loading, allow it
|
|
235
|
-
return
|
|
238
|
+
return url.startsWith('http://') || url.startsWith('https://') || url === BLANK_PAGE;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Check if a URL is safe for user-facing navigation (http/https only). */
|
|
242
|
+
function isSafeNavigationUrl(url: string): boolean {
|
|
243
|
+
return url.startsWith('http://') || url.startsWith('https://');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Minimal URL normalization for same-page comparison: root slash + default port only. */
|
|
247
|
+
function normalizeUrlForComparison(url?: string): string {
|
|
248
|
+
if (!url) return '';
|
|
249
|
+
try {
|
|
250
|
+
const parsed = new URL(url);
|
|
251
|
+
if ((parsed.protocol === 'https:' && parsed.port === '443') || (parsed.protocol === 'http:' && parsed.port === '80')) {
|
|
252
|
+
parsed.port = '';
|
|
253
|
+
}
|
|
254
|
+
const pathname = parsed.pathname === '/' ? '' : parsed.pathname;
|
|
255
|
+
return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`;
|
|
256
|
+
} catch {
|
|
257
|
+
return url;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isTargetUrl(currentUrl: string | undefined, targetUrl: string): boolean {
|
|
262
|
+
return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
|
|
236
263
|
}
|
|
237
264
|
|
|
238
265
|
/**
|
|
@@ -247,9 +274,14 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
|
|
|
247
274
|
if (tabId !== undefined) {
|
|
248
275
|
try {
|
|
249
276
|
const tab = await chrome.tabs.get(tabId);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
277
|
+
const session = automationSessions.get(workspace);
|
|
278
|
+
if (isDebuggableUrl(tab.url) && session && tab.windowId === session.windowId) return tabId;
|
|
279
|
+
if (session && tab.windowId !== session.windowId) {
|
|
280
|
+
console.warn(`[opencli] Tab ${tabId} belongs to window ${tab.windowId}, not automation window ${session.windowId}, re-resolving`);
|
|
281
|
+
} else if (!isDebuggableUrl(tab.url)) {
|
|
282
|
+
// Tab exists but URL is not debuggable — fall through to auto-resolve
|
|
283
|
+
console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
|
|
284
|
+
}
|
|
253
285
|
} catch {
|
|
254
286
|
// Tab was closed — fall through to auto-resolve
|
|
255
287
|
console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
|
|
@@ -268,7 +300,7 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
|
|
|
268
300
|
// Try to reuse by navigating to a data: URI (not interceptable by New Tab Override).
|
|
269
301
|
const reuseTab = tabs.find(t => t.id);
|
|
270
302
|
if (reuseTab?.id) {
|
|
271
|
-
await chrome.tabs.update(reuseTab.id, { url:
|
|
303
|
+
await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE });
|
|
272
304
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
273
305
|
try {
|
|
274
306
|
const updated = await chrome.tabs.get(reuseTab.id);
|
|
@@ -280,7 +312,7 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
|
|
|
280
312
|
}
|
|
281
313
|
|
|
282
314
|
// Fallback: create a new tab
|
|
283
|
-
const newTab = await chrome.tabs.create({ windowId, url:
|
|
315
|
+
const newTab = await chrome.tabs.create({ windowId, url: BLANK_PAGE, active: true });
|
|
284
316
|
if (!newTab.id) throw new Error('Failed to create tab in automation window');
|
|
285
317
|
return newTab.id;
|
|
286
318
|
}
|
|
@@ -314,54 +346,79 @@ async function handleExec(cmd: Command, workspace: string): Promise<Result> {
|
|
|
314
346
|
|
|
315
347
|
async function handleNavigate(cmd: Command, workspace: string): Promise<Result> {
|
|
316
348
|
if (!cmd.url) return { id: cmd.id, ok: false, error: 'Missing url' };
|
|
349
|
+
if (!isSafeNavigationUrl(cmd.url)) {
|
|
350
|
+
return { id: cmd.id, ok: false, error: 'Blocked URL scheme -- only http:// and https:// are allowed' };
|
|
351
|
+
}
|
|
317
352
|
const tabId = await resolveTabId(cmd.tabId, workspace);
|
|
318
353
|
|
|
319
|
-
// Capture the current URL before navigation to detect actual URL change
|
|
320
354
|
const beforeTab = await chrome.tabs.get(tabId);
|
|
321
|
-
const
|
|
355
|
+
const beforeNormalized = normalizeUrlForComparison(beforeTab.url);
|
|
322
356
|
const targetUrl = cmd.url;
|
|
323
357
|
|
|
358
|
+
// Fast-path: tab is already at the target URL and fully loaded.
|
|
359
|
+
if (beforeTab.status === 'complete' && isTargetUrl(beforeTab.url, targetUrl)) {
|
|
360
|
+
return {
|
|
361
|
+
id: cmd.id,
|
|
362
|
+
ok: true,
|
|
363
|
+
data: { title: beforeTab.title, url: beforeTab.url, tabId, timedOut: false },
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Detach any existing debugger before top-level navigation.
|
|
368
|
+
// Some sites (observed on creator.xiaohongshu.com flows) can invalidate the
|
|
369
|
+
// current inspected target during navigation, which leaves a stale CDP attach
|
|
370
|
+
// state and causes the next Runtime.evaluate to fail with
|
|
371
|
+
// "Inspected target navigated or closed". Resetting here forces a clean
|
|
372
|
+
// re-attach after navigation.
|
|
373
|
+
await executor.detach(tabId);
|
|
374
|
+
|
|
324
375
|
await chrome.tabs.update(tabId, { url: targetUrl });
|
|
325
376
|
|
|
326
|
-
// Wait
|
|
327
|
-
//
|
|
377
|
+
// Wait until navigation completes. Resolve when status is 'complete' AND either:
|
|
378
|
+
// - the URL matches the target (handles same-URL / canonicalized navigations), OR
|
|
379
|
+
// - the URL differs from the pre-navigation URL (handles redirects).
|
|
328
380
|
let timedOut = false;
|
|
329
381
|
await new Promise<void>((resolve) => {
|
|
330
|
-
let
|
|
382
|
+
let settled = false;
|
|
383
|
+
let checkTimer: ReturnType<typeof setTimeout> | null = null;
|
|
384
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
331
385
|
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
386
|
+
const finish = () => {
|
|
387
|
+
if (settled) return;
|
|
388
|
+
settled = true;
|
|
389
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
390
|
+
if (checkTimer) clearTimeout(checkTimer);
|
|
391
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
392
|
+
resolve();
|
|
393
|
+
};
|
|
334
394
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
395
|
+
const isNavigationDone = (url: string | undefined): boolean => {
|
|
396
|
+
return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized;
|
|
397
|
+
};
|
|
339
398
|
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
399
|
+
const listener = (id: number, info: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) => {
|
|
400
|
+
if (id !== tabId) return;
|
|
401
|
+
if (info.status === 'complete' && isNavigationDone(tab.url ?? info.url)) {
|
|
402
|
+
finish();
|
|
344
403
|
}
|
|
345
404
|
};
|
|
346
405
|
chrome.tabs.onUpdated.addListener(listener);
|
|
347
406
|
|
|
348
407
|
// Also check if the tab already navigated (e.g. instant cache hit)
|
|
349
|
-
setTimeout(async () => {
|
|
408
|
+
checkTimer = setTimeout(async () => {
|
|
350
409
|
try {
|
|
351
410
|
const currentTab = await chrome.tabs.get(tabId);
|
|
352
|
-
if (currentTab.
|
|
353
|
-
|
|
354
|
-
resolve();
|
|
411
|
+
if (currentTab.status === 'complete' && isNavigationDone(currentTab.url)) {
|
|
412
|
+
finish();
|
|
355
413
|
}
|
|
356
414
|
} catch { /* tab gone */ }
|
|
357
415
|
}, 100);
|
|
358
416
|
|
|
359
417
|
// Timeout fallback with warning
|
|
360
|
-
setTimeout(() => {
|
|
361
|
-
chrome.tabs.onUpdated.removeListener(listener);
|
|
418
|
+
timeoutTimer = setTimeout(() => {
|
|
362
419
|
timedOut = true;
|
|
363
420
|
console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`);
|
|
364
|
-
|
|
421
|
+
finish();
|
|
365
422
|
}, 15000);
|
|
366
423
|
});
|
|
367
424
|
|
|
@@ -388,8 +445,11 @@ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
|
|
|
388
445
|
return { id: cmd.id, ok: true, data };
|
|
389
446
|
}
|
|
390
447
|
case 'new': {
|
|
448
|
+
if (cmd.url && !isSafeNavigationUrl(cmd.url)) {
|
|
449
|
+
return { id: cmd.id, ok: false, error: 'Blocked URL scheme -- only http:// and https:// are allowed' };
|
|
450
|
+
}
|
|
391
451
|
const windowId = await getAutomationWindow(workspace);
|
|
392
|
-
const tab = await chrome.tabs.create({ windowId, url: cmd.url ??
|
|
452
|
+
const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? BLANK_PAGE, active: true });
|
|
393
453
|
return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } };
|
|
394
454
|
}
|
|
395
455
|
case 'close': {
|
|
@@ -398,18 +458,28 @@ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
|
|
|
398
458
|
const target = tabs[cmd.index];
|
|
399
459
|
if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
|
|
400
460
|
await chrome.tabs.remove(target.id);
|
|
401
|
-
executor.detach(target.id);
|
|
461
|
+
await executor.detach(target.id);
|
|
402
462
|
return { id: cmd.id, ok: true, data: { closed: target.id } };
|
|
403
463
|
}
|
|
404
464
|
const tabId = await resolveTabId(cmd.tabId, workspace);
|
|
405
465
|
await chrome.tabs.remove(tabId);
|
|
406
|
-
executor.detach(tabId);
|
|
466
|
+
await executor.detach(tabId);
|
|
407
467
|
return { id: cmd.id, ok: true, data: { closed: tabId } };
|
|
408
468
|
}
|
|
409
469
|
case 'select': {
|
|
410
470
|
if (cmd.index === undefined && cmd.tabId === undefined)
|
|
411
471
|
return { id: cmd.id, ok: false, error: 'Missing index or tabId' };
|
|
412
472
|
if (cmd.tabId !== undefined) {
|
|
473
|
+
const session = automationSessions.get(workspace);
|
|
474
|
+
let tab: chrome.tabs.Tab;
|
|
475
|
+
try {
|
|
476
|
+
tab = await chrome.tabs.get(cmd.tabId);
|
|
477
|
+
} catch {
|
|
478
|
+
return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} no longer exists` };
|
|
479
|
+
}
|
|
480
|
+
if (!session || tab.windowId !== session.windowId) {
|
|
481
|
+
return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} is not in the automation window` };
|
|
482
|
+
}
|
|
413
483
|
await chrome.tabs.update(cmd.tabId, { active: true });
|
|
414
484
|
return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
|
|
415
485
|
}
|
|
@@ -425,6 +495,9 @@ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
|
|
|
425
495
|
}
|
|
426
496
|
|
|
427
497
|
async function handleCookies(cmd: Command): Promise<Result> {
|
|
498
|
+
if (!cmd.domain && !cmd.url) {
|
|
499
|
+
return { id: cmd.id, ok: false, error: 'Cookie scope required: provide domain or url to avoid dumping all cookies' };
|
|
500
|
+
}
|
|
428
501
|
const details: chrome.cookies.GetAllDetails = {};
|
|
429
502
|
if (cmd.domain) details.domain = cmd.domain;
|
|
430
503
|
if (cmd.url) details.url = cmd.url;
|
|
@@ -481,6 +554,8 @@ async function handleSessions(cmd: Command): Promise<Result> {
|
|
|
481
554
|
}
|
|
482
555
|
|
|
483
556
|
export const __test__ = {
|
|
557
|
+
handleNavigate,
|
|
558
|
+
isTargetUrl,
|
|
484
559
|
handleTabs,
|
|
485
560
|
handleSessions,
|
|
486
561
|
getAutomationWindowId: (workspace: string = 'default') => automationSessions.get(workspace)?.windowId ?? null,
|
package/extension/src/cdp.ts
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
const attached = new Set<number>();
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/** Internal blank page used when no user URL is provided. */
|
|
12
|
+
const BLANK_PAGE = 'data:text/html,<html></html>';
|
|
13
|
+
|
|
14
|
+
/** Check if a URL can be attached via CDP — only allow http(s) and our internal blank page. */
|
|
12
15
|
function isDebuggableUrl(url?: string): boolean {
|
|
13
16
|
if (!url) return true; // empty/undefined = tab still loading, allow it
|
|
14
|
-
return
|
|
17
|
+
return url.startsWith('http://') || url.startsWith('https://') || url === BLANK_PAGE;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
async function ensureAttached(tabId: number): Promise<void> {
|
|
@@ -144,10 +147,10 @@ export async function screenshot(
|
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
export function detach(tabId: number): void {
|
|
150
|
+
export async function detach(tabId: number): Promise<void> {
|
|
148
151
|
if (!attached.has(tabId)) return;
|
|
149
152
|
attached.delete(tabId);
|
|
150
|
-
try { chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
|
|
153
|
+
try { await chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
export function registerListeners(): void {
|
|
@@ -158,12 +161,9 @@ export function registerListeners(): void {
|
|
|
158
161
|
if (source.tabId) attached.delete(source.tabId);
|
|
159
162
|
});
|
|
160
163
|
// Invalidate attached cache when tab URL changes to non-debuggable
|
|
161
|
-
chrome.tabs.onUpdated.addListener((tabId, info) => {
|
|
164
|
+
chrome.tabs.onUpdated.addListener(async (tabId, info) => {
|
|
162
165
|
if (info.url && !isDebuggableUrl(info.url)) {
|
|
163
|
-
|
|
164
|
-
attached.delete(tabId);
|
|
165
|
-
try { chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
|
|
166
|
-
}
|
|
166
|
+
await detach(tabId);
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"lint": "tsc --noEmit",
|
|
31
31
|
"prepublishOnly": "npm run build",
|
|
32
32
|
"test": "vitest run --project unit",
|
|
33
|
+
"test:adapter": "vitest run --project adapter",
|
|
33
34
|
"test:all": "vitest run",
|
|
34
35
|
"test:e2e": "vitest run --project e2e",
|
|
35
36
|
"docs:dev": "vitepress dev docs",
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
"@types/turndown": "^5.0.6",
|
|
63
64
|
"@types/ws": "^8.5.13",
|
|
64
65
|
"tsx": "^4.19.3",
|
|
65
|
-
"typescript": "^
|
|
66
|
+
"typescript": "^6.0.2",
|
|
66
67
|
"vitepress": "^1.6.4",
|
|
67
68
|
"vitest": "^4.1.0"
|
|
68
69
|
}
|
|
@@ -28,6 +28,8 @@ total=0
|
|
|
28
28
|
|
|
29
29
|
for adapter_dir in "$SRC_DIR"/*/; do
|
|
30
30
|
adapter_name="$(basename "$adapter_dir")"
|
|
31
|
+
# Skip internal directories (e.g., _shared)
|
|
32
|
+
[[ "$adapter_name" == _* ]] && continue
|
|
31
33
|
total=$((total + 1))
|
|
32
34
|
|
|
33
35
|
# Check if doc exists in browser/ or desktop/ subdirectories
|
package/src/analysis.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared API analysis helpers used by both explore.ts and record.ts.
|
|
3
|
+
*
|
|
4
|
+
* Extracts common logic for:
|
|
5
|
+
* - URL pattern normalization
|
|
6
|
+
* - Array path discovery in JSON responses
|
|
7
|
+
* - Field role detection
|
|
8
|
+
* - Auth indicator inference
|
|
9
|
+
* - Capability name inference
|
|
10
|
+
* - Strategy inference
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
VOLATILE_PARAMS,
|
|
15
|
+
SEARCH_PARAMS,
|
|
16
|
+
PAGINATION_PARAMS,
|
|
17
|
+
LIMIT_PARAMS,
|
|
18
|
+
FIELD_ROLES,
|
|
19
|
+
} from './constants.js';
|
|
20
|
+
|
|
21
|
+
// ── URL pattern normalization ───────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** Normalize a full URL into a pattern (replace IDs, strip volatile params). */
|
|
24
|
+
export function urlToPattern(url: string): string {
|
|
25
|
+
try {
|
|
26
|
+
const p = new URL(url);
|
|
27
|
+
const pathNorm = p.pathname
|
|
28
|
+
.replace(/\/\d+/g, '/{id}')
|
|
29
|
+
.replace(/\/[0-9a-fA-F]{8,}/g, '/{hex}')
|
|
30
|
+
.replace(/\/BV[a-zA-Z0-9]{10}/g, '/{bvid}');
|
|
31
|
+
const params: string[] = [];
|
|
32
|
+
p.searchParams.forEach((_v, k) => { if (!VOLATILE_PARAMS.has(k)) params.push(k); });
|
|
33
|
+
return `${p.host}${pathNorm}${params.length ? '?' + params.sort().map(k => `${k}={}`).join('&') : ''}`;
|
|
34
|
+
} catch { return url; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Array discovery in JSON responses ───────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export interface ArrayDiscovery {
|
|
40
|
+
path: string;
|
|
41
|
+
items: unknown[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Find the best (largest) array of objects in a JSON response body. */
|
|
45
|
+
export function findArrayPath(obj: unknown, depth = 0): ArrayDiscovery | null {
|
|
46
|
+
if (depth > 5 || !obj || typeof obj !== 'object') return null;
|
|
47
|
+
if (Array.isArray(obj)) {
|
|
48
|
+
if (obj.length >= 2 && obj.some(i => i && typeof i === 'object' && !Array.isArray(i))) {
|
|
49
|
+
return { path: '', items: obj };
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
let best: ArrayDiscovery | null = null;
|
|
54
|
+
for (const [key, val] of Object.entries(obj as Record<string, unknown>)) {
|
|
55
|
+
const found = findArrayPath(val, depth + 1);
|
|
56
|
+
if (found) {
|
|
57
|
+
const fullPath = found.path ? `${key}.${found.path}` : key;
|
|
58
|
+
const candidate = { path: fullPath, items: found.items };
|
|
59
|
+
if (!best || candidate.items.length > best.items.length) best = candidate;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return best;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Field flattening & role detection ───────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/** Flatten nested object keys up to maxDepth. */
|
|
68
|
+
export function flattenFields(obj: unknown, prefix: string, maxDepth: number): string[] {
|
|
69
|
+
if (maxDepth <= 0 || !obj || typeof obj !== 'object') return [];
|
|
70
|
+
const names: string[] = [];
|
|
71
|
+
const record = obj as Record<string, unknown>;
|
|
72
|
+
for (const key of Object.keys(record)) {
|
|
73
|
+
const full = prefix ? `${prefix}.${key}` : key;
|
|
74
|
+
names.push(full);
|
|
75
|
+
const val = record[key];
|
|
76
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) names.push(...flattenFields(val, full, maxDepth - 1));
|
|
77
|
+
}
|
|
78
|
+
return names;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Detect semantic field roles (title, url, author, etc.) from sample fields. */
|
|
82
|
+
export function detectFieldRoles(sampleFields: string[]): Record<string, string> {
|
|
83
|
+
const detectedFields: Record<string, string> = {};
|
|
84
|
+
for (const [role, aliases] of Object.entries(FIELD_ROLES)) {
|
|
85
|
+
for (const f of sampleFields) {
|
|
86
|
+
if (aliases.includes(f.split('.').pop()?.toLowerCase() ?? '')) {
|
|
87
|
+
detectedFields[role] = f;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return detectedFields;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Capability name inference ───────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/** Infer a CLI capability name from a URL. */
|
|
98
|
+
export function inferCapabilityName(url: string, goal?: string): string {
|
|
99
|
+
if (goal) return goal;
|
|
100
|
+
const u = url.toLowerCase();
|
|
101
|
+
if (u.includes('hot') || u.includes('popular') || u.includes('ranking') || u.includes('trending')) return 'hot';
|
|
102
|
+
if (u.includes('search')) return 'search';
|
|
103
|
+
if (u.includes('feed') || u.includes('timeline') || u.includes('dynamic')) return 'feed';
|
|
104
|
+
if (u.includes('comment') || u.includes('reply')) return 'comments';
|
|
105
|
+
if (u.includes('history')) return 'history';
|
|
106
|
+
if (u.includes('profile') || u.includes('userinfo') || u.includes('/me')) return 'me';
|
|
107
|
+
if (u.includes('favorite') || u.includes('collect') || u.includes('bookmark')) return 'favorite';
|
|
108
|
+
try {
|
|
109
|
+
const segs = new URL(url).pathname
|
|
110
|
+
.split('/')
|
|
111
|
+
.filter(s => s && !s.match(/^\d+$/) && !s.match(/^[0-9a-f]{8,}$/i) && !s.match(/^v\d+$/));
|
|
112
|
+
if (segs.length) return segs[segs.length - 1].replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
113
|
+
} catch {}
|
|
114
|
+
return 'data';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Strategy inference ──────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/** Infer auth strategy from detected indicators. */
|
|
120
|
+
export function inferStrategy(authIndicators: string[]): string {
|
|
121
|
+
if (authIndicators.includes('signature')) return 'intercept';
|
|
122
|
+
if (authIndicators.includes('bearer') || authIndicators.includes('csrf')) return 'header';
|
|
123
|
+
return 'cookie';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Auth indicator detection ────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** Detect auth indicators from HTTP headers. */
|
|
129
|
+
export function detectAuthFromHeaders(headers?: Record<string, string>): string[] {
|
|
130
|
+
if (!headers) return [];
|
|
131
|
+
const indicators: string[] = [];
|
|
132
|
+
const keys = Object.keys(headers).map(k => k.toLowerCase());
|
|
133
|
+
if (keys.some(k => k === 'authorization')) indicators.push('bearer');
|
|
134
|
+
if (keys.some(k => k.startsWith('x-csrf') || k.startsWith('x-xsrf'))) indicators.push('csrf');
|
|
135
|
+
if (keys.some(k => k.startsWith('x-s') || k === 'x-t' || k === 'x-s-common')) indicators.push('signature');
|
|
136
|
+
return indicators;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Detect auth indicators from URL and response body (heuristic). */
|
|
140
|
+
export function detectAuthFromContent(url: string, body: unknown): string[] {
|
|
141
|
+
const indicators: string[] = [];
|
|
142
|
+
if (body && typeof body === 'object') {
|
|
143
|
+
const keys = Object.keys(body as object).map(k => k.toLowerCase());
|
|
144
|
+
if (keys.some(k => k.includes('sign') || k === 'w_rid' || k.includes('token'))) {
|
|
145
|
+
indicators.push('signature');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (url.includes('/wbi/') || url.includes('w_rid=')) indicators.push('signature');
|
|
149
|
+
if (url.includes('bearer') || url.includes('access_token')) indicators.push('bearer');
|
|
150
|
+
return indicators;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Query param classification ──────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/** Extract non-volatile query params and classify them. */
|
|
156
|
+
export function classifyQueryParams(url: string): {
|
|
157
|
+
params: string[];
|
|
158
|
+
hasSearch: boolean;
|
|
159
|
+
hasPagination: boolean;
|
|
160
|
+
hasLimit: boolean;
|
|
161
|
+
} {
|
|
162
|
+
const params: string[] = [];
|
|
163
|
+
try { new URL(url).searchParams.forEach((_v, k) => { if (!VOLATILE_PARAMS.has(k)) params.push(k); }); } catch {}
|
|
164
|
+
return {
|
|
165
|
+
params,
|
|
166
|
+
hasSearch: params.some(p => SEARCH_PARAMS.has(p)),
|
|
167
|
+
hasPagination: params.some(p => PAGINATION_PARAMS.has(p)),
|
|
168
|
+
hasLimit: params.some(p => LIMIT_PARAMS.has(p)),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { MockWebSocket } = vi.hoisted(() => {
|
|
4
|
+
class MockWebSocket {
|
|
5
|
+
static OPEN = 1;
|
|
6
|
+
readyState = 1;
|
|
7
|
+
private handlers = new Map<string, Array<(...args: any[]) => void>>();
|
|
8
|
+
|
|
9
|
+
constructor(_url: string) {
|
|
10
|
+
queueMicrotask(() => this.emit('open'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
on(event: string, handler: (...args: any[]) => void): void {
|
|
14
|
+
const handlers = this.handlers.get(event) ?? [];
|
|
15
|
+
handlers.push(handler);
|
|
16
|
+
this.handlers.set(event, handlers);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
send(_message: string): void {}
|
|
20
|
+
|
|
21
|
+
close(): void {
|
|
22
|
+
this.readyState = 3;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private emit(event: string, ...args: any[]): void {
|
|
26
|
+
for (const handler of this.handlers.get(event) ?? []) {
|
|
27
|
+
handler(...args);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { MockWebSocket };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock('ws', () => ({
|
|
36
|
+
WebSocket: MockWebSocket,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
import { CDPBridge } from './cdp.js';
|
|
40
|
+
|
|
41
|
+
describe('CDPBridge cookies', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.unstubAllEnvs();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('filters cookies by actual domain match instead of substring match', async () => {
|
|
47
|
+
vi.stubEnv('OPENCLI_CDP_ENDPOINT', 'ws://127.0.0.1:9222/devtools/page/1');
|
|
48
|
+
|
|
49
|
+
const bridge = new CDPBridge();
|
|
50
|
+
vi.spyOn(bridge, 'send').mockResolvedValue({
|
|
51
|
+
cookies: [
|
|
52
|
+
{ name: 'good', value: '1', domain: '.example.com' },
|
|
53
|
+
{ name: 'exact', value: '2', domain: 'example.com' },
|
|
54
|
+
{ name: 'bad', value: '3', domain: 'notexample.com' },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const page = await bridge.connect();
|
|
59
|
+
const cookies = await page.getCookies({ domain: 'example.com' });
|
|
60
|
+
|
|
61
|
+
expect(cookies).toEqual([
|
|
62
|
+
{ name: 'good', value: '1', domain: '.example.com' },
|
|
63
|
+
{ name: 'exact', value: '2', domain: 'example.com' },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
});
|