@jackwener/opencli 1.4.0 → 1.5.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/actions/setup-chrome/action.yml +5 -4
- package/.github/workflows/build-extension.yml +2 -6
- package/.github/workflows/ci.yml +37 -3
- package/.github/workflows/e2e-headed.yml +16 -3
- package/CHANGELOG.md +23 -0
- package/PRIVACY.md +57 -0
- package/README.md +36 -7
- package/README.zh-CN.md +13 -6
- package/SKILL.md +103 -2
- package/dist/browser/cdp.d.ts +2 -1
- package/dist/browser/discover.d.ts +4 -1
- package/dist/browser/discover.js +6 -2
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +4 -12
- package/dist/browser/mcp.d.ts +2 -1
- package/dist/build-manifest.d.ts +2 -0
- package/dist/build-manifest.js +39 -14
- package/dist/build-manifest.test.js +21 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +2 -1
- package/dist/cli-manifest.json +1838 -151
- package/dist/cli.js +34 -3
- package/dist/clis/36kr/article.d.ts +1 -0
- package/dist/clis/36kr/article.js +62 -0
- package/dist/clis/36kr/hot.d.ts +3 -0
- package/dist/clis/36kr/hot.js +80 -0
- package/dist/clis/36kr/hot.test.d.ts +1 -0
- package/dist/clis/36kr/hot.test.js +15 -0
- package/dist/clis/36kr/news.d.ts +1 -0
- package/dist/clis/36kr/news.js +51 -0
- package/dist/clis/36kr/news.test.d.ts +1 -0
- package/dist/clis/36kr/news.test.js +85 -0
- package/dist/clis/36kr/search.d.ts +1 -0
- package/dist/clis/36kr/search.js +72 -0
- package/dist/clis/apple-podcasts/search.js +2 -1
- package/dist/clis/arxiv/search.js +2 -2
- package/dist/clis/bbc/news.js +0 -1
- package/dist/clis/bilibili/comments.d.ts +5 -0
- package/dist/clis/bilibili/comments.js +40 -0
- package/dist/clis/bilibili/comments.test.d.ts +1 -0
- package/dist/clis/bilibili/comments.test.js +82 -0
- package/dist/clis/chatgpt/ask.js +29 -14
- package/dist/clis/chatgpt/ax.d.ts +6 -0
- package/dist/clis/chatgpt/ax.js +172 -1
- package/dist/clis/chatgpt/model.d.ts +1 -0
- package/dist/clis/chatgpt/model.js +24 -0
- package/dist/clis/chatgpt/send.js +12 -3
- package/dist/clis/ctrip/search.js +0 -1
- package/dist/clis/douban/download.d.ts +1 -0
- package/dist/clis/douban/download.js +67 -0
- package/dist/clis/douban/download.test.d.ts +1 -0
- package/dist/clis/douban/download.test.js +170 -0
- package/dist/clis/douban/photos.d.ts +1 -0
- package/dist/clis/douban/photos.js +34 -0
- package/dist/clis/douban/utils.d.ts +25 -0
- package/dist/clis/douban/utils.js +190 -1
- package/dist/clis/douban/utils.test.d.ts +1 -0
- package/dist/clis/douban/utils.test.js +64 -0
- package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
- package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
- package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
- package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
- package/dist/clis/douyin/_shared/creation-id.js +5 -0
- package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
- package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
- package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
- package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
- package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
- package/dist/clis/douyin/_shared/sts2.js +15 -0
- package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
- package/dist/clis/douyin/_shared/text-extra.js +15 -0
- package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
- package/dist/clis/douyin/_shared/timing.d.ts +2 -0
- package/dist/clis/douyin/_shared/timing.js +22 -0
- package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/timing.test.js +28 -0
- package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
- package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
- package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
- package/dist/clis/douyin/_shared/tos-upload.js +295 -0
- package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
- package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
- package/dist/clis/douyin/_shared/transcode.js +45 -0
- package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/transcode.test.js +93 -0
- package/dist/clis/douyin/_shared/types.d.ts +26 -0
- package/dist/clis/douyin/_shared/types.js +1 -0
- package/dist/clis/douyin/activities.d.ts +1 -0
- package/dist/clis/douyin/activities.js +20 -0
- package/dist/clis/douyin/activities.test.d.ts +1 -0
- package/dist/clis/douyin/activities.test.js +22 -0
- package/dist/clis/douyin/collections.d.ts +1 -0
- package/dist/clis/douyin/collections.js +22 -0
- package/dist/clis/douyin/collections.test.d.ts +1 -0
- package/dist/clis/douyin/collections.test.js +23 -0
- package/dist/clis/douyin/delete.d.ts +1 -0
- package/dist/clis/douyin/delete.js +18 -0
- package/dist/clis/douyin/delete.test.d.ts +1 -0
- package/dist/clis/douyin/delete.test.js +11 -0
- package/dist/clis/douyin/draft.d.ts +14 -0
- package/dist/clis/douyin/draft.js +237 -0
- package/dist/clis/douyin/draft.test.d.ts +1 -0
- package/dist/clis/douyin/draft.test.js +11 -0
- package/dist/clis/douyin/drafts.d.ts +1 -0
- package/dist/clis/douyin/drafts.js +23 -0
- package/dist/clis/douyin/drafts.test.d.ts +1 -0
- package/dist/clis/douyin/drafts.test.js +11 -0
- package/dist/clis/douyin/hashtag.d.ts +1 -0
- package/dist/clis/douyin/hashtag.js +45 -0
- package/dist/clis/douyin/hashtag.test.d.ts +1 -0
- package/dist/clis/douyin/hashtag.test.js +25 -0
- package/dist/clis/douyin/location.d.ts +1 -0
- package/dist/clis/douyin/location.js +24 -0
- package/dist/clis/douyin/location.test.d.ts +1 -0
- package/dist/clis/douyin/location.test.js +23 -0
- package/dist/clis/douyin/profile.d.ts +1 -0
- package/dist/clis/douyin/profile.js +28 -0
- package/dist/clis/douyin/profile.test.d.ts +1 -0
- package/dist/clis/douyin/profile.test.js +11 -0
- package/dist/clis/douyin/publish.d.ts +14 -0
- package/dist/clis/douyin/publish.js +288 -0
- package/dist/clis/douyin/publish.test.d.ts +1 -0
- package/dist/clis/douyin/publish.test.js +38 -0
- package/dist/clis/douyin/stats.d.ts +1 -0
- package/dist/clis/douyin/stats.js +27 -0
- package/dist/clis/douyin/stats.test.d.ts +1 -0
- package/dist/clis/douyin/stats.test.js +22 -0
- package/dist/clis/douyin/update.d.ts +1 -0
- package/dist/clis/douyin/update.js +31 -0
- package/dist/clis/douyin/update.test.d.ts +1 -0
- package/dist/clis/douyin/update.test.js +11 -0
- package/dist/clis/douyin/videos.d.ts +1 -0
- package/dist/clis/douyin/videos.js +34 -0
- package/dist/clis/douyin/videos.test.d.ts +1 -0
- package/dist/clis/douyin/videos.test.js +11 -0
- package/dist/clis/hackernews/search.yaml +1 -1
- package/dist/clis/imdb/person.d.ts +1 -0
- package/dist/clis/imdb/person.js +203 -0
- package/dist/clis/imdb/reviews.d.ts +1 -0
- package/dist/clis/imdb/reviews.js +88 -0
- package/dist/clis/imdb/search.d.ts +1 -0
- package/dist/clis/imdb/search.js +161 -0
- package/dist/clis/imdb/title.d.ts +1 -0
- package/dist/clis/imdb/title.js +93 -0
- package/dist/clis/imdb/top.d.ts +1 -0
- package/dist/clis/imdb/top.js +53 -0
- package/dist/clis/imdb/trending.d.ts +1 -0
- package/dist/clis/imdb/trending.js +52 -0
- package/dist/clis/imdb/utils.d.ts +46 -0
- package/dist/clis/imdb/utils.js +285 -0
- package/dist/clis/imdb/utils.test.d.ts +1 -0
- package/dist/clis/imdb/utils.test.js +88 -0
- package/dist/clis/instagram/search.yaml +2 -1
- package/dist/clis/jd/item.d.ts +4 -0
- package/dist/clis/jd/item.js +16 -15
- package/dist/clis/jd/item.test.js +16 -1
- package/dist/clis/linux-do/categories.yaml +38 -9
- package/dist/clis/linux-do/category.d.ts +1 -0
- package/dist/clis/linux-do/category.js +36 -0
- package/dist/clis/linux-do/feed.d.ts +45 -0
- package/dist/clis/linux-do/feed.js +397 -0
- package/dist/clis/linux-do/feed.test.d.ts +1 -0
- package/dist/clis/linux-do/feed.test.js +118 -0
- package/dist/clis/linux-do/hot.d.ts +1 -0
- package/dist/clis/linux-do/hot.js +25 -0
- package/dist/clis/linux-do/latest.d.ts +1 -0
- package/dist/clis/linux-do/latest.js +18 -0
- package/dist/clis/linux-do/search.yaml +3 -1
- package/dist/clis/linux-do/tags.yaml +41 -0
- package/dist/clis/linux-do/topic.yaml +41 -3
- package/dist/clis/linux-do/user-posts.yaml +67 -0
- package/dist/clis/linux-do/user-topics.yaml +54 -0
- package/dist/clis/medium/search.js +1 -1
- package/dist/clis/paperreview/commands.test.d.ts +3 -0
- package/dist/clis/paperreview/commands.test.js +243 -0
- package/dist/clis/paperreview/feedback.d.ts +1 -0
- package/dist/clis/paperreview/feedback.js +52 -0
- package/dist/clis/paperreview/review.d.ts +1 -0
- package/dist/clis/paperreview/review.js +37 -0
- package/dist/clis/paperreview/submit.d.ts +1 -0
- package/dist/clis/paperreview/submit.js +85 -0
- package/dist/clis/paperreview/utils.d.ts +46 -0
- package/dist/clis/paperreview/utils.js +197 -0
- package/dist/clis/paperreview/utils.test.d.ts +1 -0
- package/dist/clis/paperreview/utils.test.js +49 -0
- package/dist/clis/producthunt/browse.d.ts +1 -0
- package/dist/clis/producthunt/browse.js +99 -0
- package/dist/clis/producthunt/hot.d.ts +1 -0
- package/dist/clis/producthunt/hot.js +110 -0
- package/dist/clis/producthunt/posts.d.ts +1 -0
- package/dist/clis/producthunt/posts.js +28 -0
- package/dist/clis/producthunt/today.d.ts +1 -0
- package/dist/clis/producthunt/today.js +35 -0
- package/dist/clis/producthunt/utils.d.ts +29 -0
- package/dist/clis/producthunt/utils.js +99 -0
- package/dist/clis/producthunt/utils.test.d.ts +1 -0
- package/dist/clis/producthunt/utils.test.js +64 -0
- package/dist/clis/reuters/search.js +0 -1
- package/dist/clis/twitter/article.js +4 -28
- package/dist/clis/twitter/likes.d.ts +24 -0
- package/dist/clis/twitter/likes.js +217 -0
- package/dist/clis/twitter/likes.test.d.ts +1 -0
- package/dist/clis/twitter/likes.test.js +85 -0
- package/dist/clis/twitter/profile.js +4 -28
- package/dist/clis/twitter/search.js +7 -4
- package/dist/clis/twitter/search.test.js +56 -2
- package/dist/clis/twitter/shared.d.ts +6 -0
- package/dist/clis/twitter/shared.js +35 -0
- package/dist/clis/twitter/timeline.js +2 -13
- package/dist/clis/weibo/comments.d.ts +1 -0
- package/dist/clis/weibo/comments.js +53 -0
- package/dist/clis/weibo/feed.d.ts +1 -0
- package/dist/clis/weibo/feed.js +56 -0
- package/dist/clis/weibo/hot.js +0 -1
- package/dist/clis/weibo/me.d.ts +1 -0
- package/dist/clis/weibo/me.js +76 -0
- package/dist/clis/weibo/post.d.ts +1 -0
- package/dist/clis/weibo/post.js +75 -0
- package/dist/clis/weibo/user.d.ts +1 -0
- package/dist/clis/weibo/user.js +63 -0
- package/dist/clis/weibo/utils.d.ts +6 -0
- package/dist/clis/weibo/utils.js +30 -0
- package/dist/clis/weixin/download.d.ts +17 -0
- package/dist/clis/weixin/download.js +88 -20
- package/dist/clis/weread/book.js +2 -2
- package/dist/clis/weread/commands.test.d.ts +3 -0
- package/dist/clis/weread/commands.test.js +43 -0
- package/dist/clis/weread/highlights.js +2 -2
- package/dist/clis/weread/notebooks.js +2 -2
- package/dist/clis/weread/notes.js +3 -3
- package/dist/clis/weread/search.js +3 -2
- package/dist/clis/weread/shelf.js +2 -2
- package/dist/clis/weread/utils.d.ts +4 -4
- package/dist/clis/weread/utils.js +32 -14
- package/dist/clis/weread/utils.test.js +1 -28
- package/dist/clis/xiaohongshu/comments.d.ts +5 -0
- package/dist/clis/xiaohongshu/comments.js +74 -0
- package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/comments.test.js +79 -0
- package/dist/clis/xiaohongshu/publish.js +114 -18
- package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +119 -0
- package/dist/clis/xueqiu/search.yaml +2 -1
- package/dist/clis/yahoo-finance/quote.js +0 -1
- package/dist/clis/youtube/channel.d.ts +1 -0
- package/dist/clis/youtube/channel.js +150 -0
- package/dist/clis/youtube/comments.d.ts +1 -0
- package/dist/clis/youtube/comments.js +95 -0
- package/dist/clis/youtube/search.js +0 -1
- package/dist/clis/zhihu/search.yaml +2 -1
- package/dist/commanderAdapter.d.ts +1 -0
- package/dist/commanderAdapter.js +176 -29
- package/dist/commanderAdapter.test.d.ts +1 -0
- package/dist/commanderAdapter.test.js +62 -0
- package/dist/daemon.js +17 -1
- package/dist/discovery.js +8 -14
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +9 -2
- package/dist/download/index.js +63 -51
- package/dist/download/index.test.js +17 -4
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +15 -32
- package/dist/execution.d.ts +1 -3
- package/dist/execution.js +21 -1
- package/dist/external-clis.yaml +0 -17
- package/dist/hooks.js +2 -0
- package/dist/main.js +5 -0
- package/dist/output.js +5 -1
- package/dist/pipeline/executor.js +3 -4
- package/dist/plugin-manifest.d.ts +70 -0
- package/dist/plugin-manifest.js +160 -0
- package/dist/plugin-manifest.test.d.ts +4 -0
- package/dist/plugin-manifest.test.js +179 -0
- package/dist/plugin.d.ts +38 -5
- package/dist/plugin.js +267 -33
- package/dist/plugin.test.js +220 -3
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runtime-detect.d.ts +21 -0
- package/dist/runtime-detect.js +32 -0
- package/dist/runtime-detect.test.d.ts +1 -0
- package/dist/runtime-detect.test.js +27 -0
- package/dist/runtime.js +1 -1
- package/dist/serialization.d.ts +2 -0
- package/dist/serialization.js +6 -0
- package/dist/types.d.ts +1 -0
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +112 -0
- package/dist/weixin-download.test.d.ts +1 -0
- package/dist/weixin-download.test.js +30 -0
- package/dist/weread-private-api-regression.test.d.ts +1 -0
- package/dist/weread-private-api-regression.test.js +122 -0
- package/dist/weread-search-regression.test.d.ts +1 -0
- package/dist/weread-search-regression.test.js +39 -0
- package/dist/yaml-schema.d.ts +3 -0
- package/dist/yaml-schema.js +18 -1
- package/docs/.vitepress/config.mts +17 -0
- package/docs/adapters/browser/36kr.md +47 -0
- package/docs/adapters/browser/douban.md +14 -0
- package/docs/adapters/browser/douyin.md +75 -0
- package/docs/adapters/browser/imdb.md +47 -0
- package/docs/adapters/browser/jd.md +2 -2
- package/docs/adapters/browser/linux-do.md +181 -20
- package/docs/adapters/browser/paperreview.md +43 -0
- package/docs/adapters/browser/producthunt.md +49 -0
- package/docs/adapters/browser/twitter.md +6 -0
- package/docs/adapters/desktop/chatgpt.md +5 -0
- package/docs/adapters/index.md +12 -3
- package/docs/advanced/download.md +4 -0
- package/docs/advanced/rate-limiter-plugin.md +99 -0
- package/docs/guide/electron-app-cli.md +200 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/plugins.md +87 -0
- package/docs/zh/guide/electron-app-cli.md +188 -0
- package/docs/zh/guide/getting-started.md +1 -0
- package/docs/zh/guide/plugins.md +65 -0
- package/extension/dist/background.js +508 -518
- package/extension/manifest.json +6 -2
- package/extension/package.json +2 -1
- package/extension/popup.html +84 -0
- package/extension/popup.js +25 -0
- package/extension/scripts/package-release.mjs +179 -0
- package/extension/src/background.ts +22 -1
- package/package.json +4 -1
- package/scripts/postinstall.js +10 -0
- package/src/browser/cdp.ts +2 -1
- package/src/browser/discover.ts +8 -3
- package/src/browser/errors.ts +13 -14
- package/src/browser/mcp.ts +2 -1
- package/src/build-manifest.test.ts +23 -0
- package/src/build-manifest.ts +40 -15
- package/src/capabilityRouting.ts +2 -1
- package/src/cli.ts +35 -3
- package/src/clis/36kr/article.ts +69 -0
- package/src/clis/36kr/hot.test.ts +19 -0
- package/src/clis/36kr/hot.ts +100 -0
- package/src/clis/36kr/news.test.ts +90 -0
- package/src/clis/36kr/news.ts +54 -0
- package/src/clis/36kr/search.ts +78 -0
- package/src/clis/apple-podcasts/search.ts +2 -1
- package/src/clis/arxiv/search.ts +2 -2
- package/src/clis/bbc/news.ts +0 -1
- package/src/clis/bilibili/comments.test.ts +102 -0
- package/src/clis/bilibili/comments.ts +44 -0
- package/src/clis/chatgpt/ask.ts +28 -14
- package/src/clis/chatgpt/ax.ts +180 -1
- package/src/clis/chatgpt/model.ts +27 -0
- package/src/clis/chatgpt/send.ts +16 -6
- package/src/clis/ctrip/search.ts +0 -1
- package/src/clis/douban/download.test.ts +196 -0
- package/src/clis/douban/download.ts +78 -0
- package/src/clis/douban/photos.ts +36 -0
- package/src/clis/douban/utils.test.ts +97 -0
- package/src/clis/douban/utils.ts +232 -1
- package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
- package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
- package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
- package/src/clis/douyin/_shared/creation-id.ts +8 -0
- package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
- package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
- package/src/clis/douyin/_shared/sts2.ts +20 -0
- package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
- package/src/clis/douyin/_shared/text-extra.ts +33 -0
- package/src/clis/douyin/_shared/timing.test.ts +38 -0
- package/src/clis/douyin/_shared/timing.ts +22 -0
- package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
- package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
- package/src/clis/douyin/_shared/tos-upload.ts +444 -0
- package/src/clis/douyin/_shared/transcode.test.ts +117 -0
- package/src/clis/douyin/_shared/transcode.ts +78 -0
- package/src/clis/douyin/_shared/types.ts +29 -0
- package/src/clis/douyin/activities.test.ts +25 -0
- package/src/clis/douyin/activities.ts +23 -0
- package/src/clis/douyin/collections.test.ts +26 -0
- package/src/clis/douyin/collections.ts +25 -0
- package/src/clis/douyin/delete.test.ts +12 -0
- package/src/clis/douyin/delete.ts +20 -0
- package/src/clis/douyin/draft.test.ts +12 -0
- package/src/clis/douyin/draft.ts +282 -0
- package/src/clis/douyin/drafts.test.ts +12 -0
- package/src/clis/douyin/drafts.ts +27 -0
- package/src/clis/douyin/hashtag.test.ts +28 -0
- package/src/clis/douyin/hashtag.ts +56 -0
- package/src/clis/douyin/location.test.ts +26 -0
- package/src/clis/douyin/location.ts +27 -0
- package/src/clis/douyin/profile.test.ts +12 -0
- package/src/clis/douyin/profile.ts +37 -0
- package/src/clis/douyin/publish.test.ts +45 -0
- package/src/clis/douyin/publish.ts +340 -0
- package/src/clis/douyin/stats.test.ts +25 -0
- package/src/clis/douyin/stats.ts +30 -0
- package/src/clis/douyin/update.test.ts +12 -0
- package/src/clis/douyin/update.ts +43 -0
- package/src/clis/douyin/videos.test.ts +12 -0
- package/src/clis/douyin/videos.ts +49 -0
- package/src/clis/hackernews/search.yaml +1 -1
- package/src/clis/imdb/person.ts +232 -0
- package/src/clis/imdb/reviews.ts +111 -0
- package/src/clis/imdb/search.ts +179 -0
- package/src/clis/imdb/title.ts +121 -0
- package/src/clis/imdb/top.ts +67 -0
- package/src/clis/imdb/trending.ts +66 -0
- package/src/clis/imdb/utils.test.ts +117 -0
- package/src/clis/imdb/utils.ts +305 -0
- package/src/clis/instagram/search.yaml +2 -1
- package/src/clis/jd/item.test.ts +18 -1
- package/src/clis/jd/item.ts +18 -15
- package/src/clis/linux-do/categories.yaml +38 -9
- package/src/clis/linux-do/category.ts +37 -0
- package/src/clis/linux-do/feed.test.ts +132 -0
- package/src/clis/linux-do/feed.ts +501 -0
- package/src/clis/linux-do/hot.ts +26 -0
- package/src/clis/linux-do/latest.ts +19 -0
- package/src/clis/linux-do/search.yaml +3 -1
- package/src/clis/linux-do/tags.yaml +41 -0
- package/src/clis/linux-do/topic.yaml +41 -3
- package/src/clis/linux-do/user-posts.yaml +67 -0
- package/src/clis/linux-do/user-topics.yaml +54 -0
- package/src/clis/medium/search.ts +1 -1
- package/src/clis/paperreview/commands.test.ts +283 -0
- package/src/clis/paperreview/feedback.ts +64 -0
- package/src/clis/paperreview/review.ts +47 -0
- package/src/clis/paperreview/submit.ts +119 -0
- package/src/clis/paperreview/utils.test.ts +68 -0
- package/src/clis/paperreview/utils.ts +276 -0
- package/src/clis/producthunt/browse.ts +109 -0
- package/src/clis/producthunt/hot.ts +127 -0
- package/src/clis/producthunt/posts.ts +29 -0
- package/src/clis/producthunt/today.ts +37 -0
- package/src/clis/producthunt/utils.test.ts +72 -0
- package/src/clis/producthunt/utils.ts +122 -0
- package/src/clis/reuters/search.ts +0 -1
- package/src/clis/twitter/article.ts +5 -28
- package/src/clis/twitter/likes.test.ts +91 -0
- package/src/clis/twitter/likes.ts +256 -0
- package/src/clis/twitter/profile.ts +5 -28
- package/src/clis/twitter/search.test.ts +71 -2
- package/src/clis/twitter/search.ts +8 -4
- package/src/clis/twitter/shared.ts +45 -0
- package/src/clis/twitter/timeline.ts +2 -13
- package/src/clis/weibo/comments.ts +54 -0
- package/src/clis/weibo/feed.ts +57 -0
- package/src/clis/weibo/hot.ts +0 -1
- package/src/clis/weibo/me.ts +77 -0
- package/src/clis/weibo/post.ts +77 -0
- package/src/clis/weibo/user.ts +64 -0
- package/src/clis/weibo/utils.ts +32 -0
- package/src/clis/weixin/download.ts +114 -20
- package/src/clis/weread/book.ts +2 -2
- package/src/clis/weread/commands.test.ts +57 -0
- package/src/clis/weread/highlights.ts +2 -2
- package/src/clis/weread/notebooks.ts +2 -2
- package/src/clis/weread/notes.ts +3 -3
- package/src/clis/weread/search.ts +3 -2
- package/src/clis/weread/shelf.ts +2 -2
- package/src/clis/weread/utils.test.ts +1 -32
- package/src/clis/weread/utils.ts +41 -16
- package/src/clis/xiaohongshu/comments.test.ts +96 -0
- package/src/clis/xiaohongshu/comments.ts +81 -0
- package/src/clis/xiaohongshu/publish.test.ts +137 -0
- package/src/clis/xiaohongshu/publish.ts +129 -18
- package/src/clis/xueqiu/search.yaml +2 -1
- package/src/clis/yahoo-finance/quote.ts +0 -1
- package/src/clis/youtube/channel.ts +155 -0
- package/src/clis/youtube/comments.ts +97 -0
- package/src/clis/youtube/search.ts +0 -1
- package/src/clis/zhihu/search.yaml +2 -1
- package/src/commanderAdapter.test.ts +78 -0
- package/src/commanderAdapter.ts +188 -24
- package/src/daemon.ts +19 -1
- package/src/discovery.ts +8 -15
- package/src/doctor.ts +13 -2
- package/src/download/index.test.ts +14 -4
- package/src/download/index.ts +67 -55
- package/src/errors.ts +25 -66
- package/src/execution.ts +28 -3
- package/src/external-clis.yaml +0 -17
- package/src/hooks.ts +1 -0
- package/src/main.ts +6 -0
- package/src/output.ts +3 -1
- package/src/pipeline/executor.ts +4 -6
- package/src/plugin-manifest.test.ts +223 -0
- package/src/plugin-manifest.ts +206 -0
- package/src/plugin.test.ts +246 -2
- package/src/plugin.ts +338 -36
- package/src/registry.ts +6 -1
- package/src/runtime-detect.test.ts +30 -0
- package/src/runtime-detect.ts +36 -0
- package/src/runtime.ts +1 -1
- package/src/serialization.ts +4 -0
- package/src/types.ts +1 -0
- package/src/update-check.ts +114 -0
- package/src/weixin-download.test.ts +64 -0
- package/src/weread-private-api-regression.test.ts +150 -0
- package/src/weread-search-regression.test.ts +44 -0
- package/src/yaml-schema.ts +20 -0
- package/tests/e2e/browser-auth.test.ts +13 -9
- package/tests/e2e/browser-public-extended.test.ts +162 -0
- package/tests/e2e/browser-public.test.ts +55 -136
- package/tests/e2e/helpers.ts +2 -1
- package/tests/e2e/public-commands.test.ts +37 -3
- package/tests/smoke/api-health.test.ts +1 -1
- package/vitest.config.ts +34 -17
- package/dist/clis/linux-do/category.yaml +0 -51
- package/dist/clis/linux-do/hot.yaml +0 -50
- package/dist/clis/linux-do/latest.yaml +0 -40
- package/src/clis/linux-do/category.yaml +0 -51
- package/src/clis/linux-do/hot.yaml +0 -50
- package/src/clis/linux-do/latest.yaml +0 -40
package/dist/cli.js
CHANGED
|
@@ -234,9 +234,19 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
234
234
|
const { installPlugin } = await import('./plugin.js');
|
|
235
235
|
const { discoverPlugins } = await import('./discovery.js');
|
|
236
236
|
try {
|
|
237
|
-
const
|
|
237
|
+
const result = installPlugin(source);
|
|
238
238
|
await discoverPlugins();
|
|
239
|
-
|
|
239
|
+
if (Array.isArray(result)) {
|
|
240
|
+
if (result.length === 0) {
|
|
241
|
+
console.log(chalk.yellow('No plugins were installed (all skipped or incompatible).'));
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.log(chalk.green(`\u2705 Installed ${result.length} plugin(s) from monorepo: ${result.join(', ')}`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.log(chalk.green(`\u2705 Plugin "${result}" installed successfully. Commands are ready to use.`));
|
|
249
|
+
}
|
|
240
250
|
}
|
|
241
251
|
catch (err) {
|
|
242
252
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
@@ -339,11 +349,32 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
339
349
|
console.log();
|
|
340
350
|
console.log(chalk.bold(' Installed plugins'));
|
|
341
351
|
console.log();
|
|
352
|
+
// Group by monorepo
|
|
353
|
+
const standalone = plugins.filter((p) => !p.monorepoName);
|
|
354
|
+
const monoGroups = new Map();
|
|
342
355
|
for (const p of plugins) {
|
|
356
|
+
if (!p.monorepoName)
|
|
357
|
+
continue;
|
|
358
|
+
const g = monoGroups.get(p.monorepoName) ?? [];
|
|
359
|
+
g.push(p);
|
|
360
|
+
monoGroups.set(p.monorepoName, g);
|
|
361
|
+
}
|
|
362
|
+
for (const p of standalone) {
|
|
343
363
|
const version = p.version ? chalk.green(` @${p.version}`) : '';
|
|
364
|
+
const desc = p.description ? chalk.dim(` — ${p.description}`) : '';
|
|
344
365
|
const cmds = p.commands.length > 0 ? chalk.dim(` (${p.commands.join(', ')})`) : '';
|
|
345
366
|
const src = p.source ? chalk.dim(` ← ${p.source}`) : '';
|
|
346
|
-
console.log(` ${chalk.cyan(p.name)}${version}${cmds}${src}`);
|
|
367
|
+
console.log(` ${chalk.cyan(p.name)}${version}${desc}${cmds}${src}`);
|
|
368
|
+
}
|
|
369
|
+
for (const [mono, group] of monoGroups) {
|
|
370
|
+
console.log();
|
|
371
|
+
console.log(chalk.bold.magenta(` 📦 ${mono}`) + chalk.dim(' (monorepo)'));
|
|
372
|
+
for (const p of group) {
|
|
373
|
+
const version = p.version ? chalk.green(` @${p.version}`) : '';
|
|
374
|
+
const desc = p.description ? chalk.dim(` — ${p.description}`) : '';
|
|
375
|
+
const cmds = p.commands.length > 0 ? chalk.dim(` (${p.commands.join(', ')})`) : '';
|
|
376
|
+
console.log(` ${chalk.cyan(p.name)}${version}${desc}${cmds}`);
|
|
377
|
+
}
|
|
347
378
|
}
|
|
348
379
|
console.log();
|
|
349
380
|
console.log(chalk.dim(` ${plugins.length} plugin(s) installed`));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 36kr article detail — INTERCEPT strategy.
|
|
3
|
+
*
|
|
4
|
+
* Fetches the full content of a 36kr article given its ID or URL.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
/** Extract article ID from a full URL or a bare numeric ID string */
|
|
9
|
+
function parseArticleId(input) {
|
|
10
|
+
const m = input.match(/\/p\/(\d+)/);
|
|
11
|
+
return m ? m[1] : input.replace(/\D/g, '');
|
|
12
|
+
}
|
|
13
|
+
cli({
|
|
14
|
+
site: '36kr',
|
|
15
|
+
name: 'article',
|
|
16
|
+
description: '获取36氪文章正文内容',
|
|
17
|
+
domain: 'www.36kr.com',
|
|
18
|
+
strategy: Strategy.INTERCEPT,
|
|
19
|
+
args: [
|
|
20
|
+
{ name: 'id', positional: true, required: true, help: 'Article ID or full 36kr article URL' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['field', 'value'],
|
|
23
|
+
func: async (page, args) => {
|
|
24
|
+
const articleId = parseArticleId(String(args.id ?? ''));
|
|
25
|
+
if (!articleId) {
|
|
26
|
+
throw new CliError('INVALID_ARGUMENT', 'Invalid article ID or URL');
|
|
27
|
+
}
|
|
28
|
+
await page.installInterceptor('36kr.com/api');
|
|
29
|
+
await page.goto(`https://www.36kr.com/p/${articleId}`);
|
|
30
|
+
await page.wait(5);
|
|
31
|
+
const data = await page.evaluate(`
|
|
32
|
+
(() => {
|
|
33
|
+
// Title: 36kr uses class "article-title" on h1
|
|
34
|
+
const title = document.querySelector('.article-title, h1')?.textContent?.trim() || '';
|
|
35
|
+
// Author: second .author-name (first is empty nav link, second has real name)
|
|
36
|
+
const authorEls = document.querySelectorAll('.author-name');
|
|
37
|
+
const author = Array.from(authorEls).map(el => el.textContent?.trim()).filter(Boolean)[0] || '';
|
|
38
|
+
// Date: 36kr uses class "title-icon-item item-time" for the publish date
|
|
39
|
+
const dateRaw = document.querySelector('.item-time')?.textContent?.trim() || '';
|
|
40
|
+
const date = dateRaw.replace(/^[·\s]+/, '').trim();
|
|
41
|
+
// Article body paragraphs
|
|
42
|
+
const bodyEls = document.querySelectorAll('[class*="article-content"] p, [class*="rich-text"] p, .article p');
|
|
43
|
+
const body = Array.from(bodyEls)
|
|
44
|
+
.map(el => el.textContent?.trim())
|
|
45
|
+
.filter(t => t && t.length > 10)
|
|
46
|
+
.join(' ')
|
|
47
|
+
.slice(0, 800);
|
|
48
|
+
return { title, author, date, body };
|
|
49
|
+
})()
|
|
50
|
+
`);
|
|
51
|
+
if (!data?.title) {
|
|
52
|
+
throw new CliError('NOT_FOUND', 'Article not found or failed to load', 'Check the article ID');
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
{ field: 'title', value: data.title },
|
|
56
|
+
{ field: 'author', value: data.author || '-' },
|
|
57
|
+
{ field: 'date', value: data.date || '-' },
|
|
58
|
+
{ field: 'url', value: `https://36kr.com/p/${articleId}` },
|
|
59
|
+
{ field: 'body', value: data.body || '-' },
|
|
60
|
+
];
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 36kr hot-list — INTERCEPT strategy.
|
|
3
|
+
*
|
|
4
|
+
* Navigates to the 36kr hot-list page and scrapes rendered article links.
|
|
5
|
+
* Supports category types: renqi (人气), zonghe (综合), shoucang (收藏), catalog (综合热门).
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { CliError } from '../../errors.js';
|
|
9
|
+
const TYPE_MAP = {
|
|
10
|
+
renqi: '人气榜',
|
|
11
|
+
zonghe: '综合榜',
|
|
12
|
+
shoucang: '收藏榜',
|
|
13
|
+
catalog: '热门资讯',
|
|
14
|
+
};
|
|
15
|
+
function getShanghaiDate(date = new Date()) {
|
|
16
|
+
// Shanghai stays on UTC+8 year-round, so a fixed offset is sufficient here
|
|
17
|
+
// and avoids the slow Intl timezone path that timed out on Windows CI.
|
|
18
|
+
return new Date(date.getTime() + 8 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
19
|
+
}
|
|
20
|
+
function buildHotListUrl(listType, date = new Date()) {
|
|
21
|
+
if (listType === 'catalog') {
|
|
22
|
+
return 'https://www.36kr.com/hot-list/catalog';
|
|
23
|
+
}
|
|
24
|
+
return `https://www.36kr.com/hot-list/${listType}/${getShanghaiDate(date)}/1`;
|
|
25
|
+
}
|
|
26
|
+
cli({
|
|
27
|
+
site: '36kr',
|
|
28
|
+
name: 'hot',
|
|
29
|
+
description: '36氪热榜 — trending articles (renqi/zonghe/shoucang/catalog)',
|
|
30
|
+
domain: 'www.36kr.com',
|
|
31
|
+
strategy: Strategy.INTERCEPT,
|
|
32
|
+
args: [
|
|
33
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of items (max 50)' },
|
|
34
|
+
{
|
|
35
|
+
name: 'type',
|
|
36
|
+
type: 'string',
|
|
37
|
+
default: 'catalog',
|
|
38
|
+
help: 'List type: renqi (人气), zonghe (综合), shoucang (收藏), catalog (热门资讯)',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
columns: ['rank', 'title', 'url'],
|
|
42
|
+
func: async (page, args) => {
|
|
43
|
+
const count = Math.min(Number(args.limit) || 20, 50);
|
|
44
|
+
const listType = String(args.type ?? 'catalog');
|
|
45
|
+
if (!TYPE_MAP[listType]) {
|
|
46
|
+
throw new CliError('INVALID_ARGUMENT', `Unknown type "${listType}". Valid types: ${Object.keys(TYPE_MAP).join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
const url = buildHotListUrl(listType);
|
|
49
|
+
await page.installInterceptor('36kr.com/api');
|
|
50
|
+
await page.goto(url);
|
|
51
|
+
await page.wait(6);
|
|
52
|
+
// Scrape rendered article links from DOM (deduplicated)
|
|
53
|
+
const domItems = await page.evaluate(`
|
|
54
|
+
(() => {
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const results = [];
|
|
57
|
+
const links = document.querySelectorAll('a[href*="/p/"]');
|
|
58
|
+
for (const el of links) {
|
|
59
|
+
const href = el.getAttribute('href') || '';
|
|
60
|
+
const title = el.textContent?.trim() || '';
|
|
61
|
+
if (!title || title.length < 5 || seen.has(href) || seen.has(title)) continue;
|
|
62
|
+
seen.add(href);
|
|
63
|
+
seen.add(title);
|
|
64
|
+
results.push({ title, url: href.startsWith('http') ? href : 'https://36kr.com' + href });
|
|
65
|
+
}
|
|
66
|
+
return results;
|
|
67
|
+
})()
|
|
68
|
+
`);
|
|
69
|
+
const items = Array.isArray(domItems) ? domItems : [];
|
|
70
|
+
if (items.length === 0) {
|
|
71
|
+
throw new CliError('NO_DATA', 'Could not retrieve 36kr hot list', '36kr may have changed its DOM structure');
|
|
72
|
+
}
|
|
73
|
+
return items.slice(0, count).map((item, i) => ({
|
|
74
|
+
rank: i + 1,
|
|
75
|
+
title: item.title,
|
|
76
|
+
url: item.url,
|
|
77
|
+
}));
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
export { buildHotListUrl, getShanghaiDate };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildHotListUrl, getShanghaiDate } from './hot.js';
|
|
3
|
+
describe('36kr/hot date routing', () => {
|
|
4
|
+
it('formats dates in Asia/Shanghai instead of UTC', () => {
|
|
5
|
+
const date = new Date('2026-03-25T18:30:00.000Z');
|
|
6
|
+
expect(getShanghaiDate(date)).toBe('2026-03-26');
|
|
7
|
+
});
|
|
8
|
+
it('builds dated hot-list routes with Shanghai-local date', () => {
|
|
9
|
+
const date = new Date('2026-03-25T18:30:00.000Z');
|
|
10
|
+
expect(buildHotListUrl('renqi', date)).toBe('https://www.36kr.com/hot-list/renqi/2026-03-26/1');
|
|
11
|
+
});
|
|
12
|
+
it('keeps catalog on the static route', () => {
|
|
13
|
+
expect(buildHotListUrl('catalog')).toBe('https://www.36kr.com/hot-list/catalog');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 36kr latest news — public RSS feed, no browser needed.
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
cli({
|
|
6
|
+
site: '36kr',
|
|
7
|
+
name: 'news',
|
|
8
|
+
description: 'Latest tech/startup news from 36kr (36氪)',
|
|
9
|
+
domain: 'www.36kr.com',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of articles (max 50)' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['rank', 'title', 'summary', 'date', 'url'],
|
|
15
|
+
func: async (_page, kwargs) => {
|
|
16
|
+
const count = Math.min(kwargs.limit || 20, 50);
|
|
17
|
+
const resp = await fetch('https://www.36kr.com/feed', {
|
|
18
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; opencli/1.0)' },
|
|
19
|
+
});
|
|
20
|
+
if (!resp.ok)
|
|
21
|
+
return [];
|
|
22
|
+
const xml = await resp.text();
|
|
23
|
+
const items = [];
|
|
24
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = itemRegex.exec(xml)) && items.length < count) {
|
|
27
|
+
const block = match[1];
|
|
28
|
+
const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? '';
|
|
29
|
+
const url = block.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1] ??
|
|
30
|
+
block.match(/<link>(.*?)<\/link>/)?.[1] ??
|
|
31
|
+
'';
|
|
32
|
+
const pubDate = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() ?? '';
|
|
33
|
+
const date = pubDate.slice(0, 10);
|
|
34
|
+
// Extract plain-text summary from HTML description (first ~120 chars)
|
|
35
|
+
const rawDesc = block.match(/<description><!\[CDATA\[([\s\S]*?)\]\]>/)?.[1] ?? '';
|
|
36
|
+
const summary = rawDesc
|
|
37
|
+
.replace(/<[^>]+>/g, ' ')
|
|
38
|
+
.replace(/ /g, ' ')
|
|
39
|
+
.replace(/&/g, '&')
|
|
40
|
+
.replace(/</g, '<')
|
|
41
|
+
.replace(/>/g, '>')
|
|
42
|
+
.replace(/\s+/g, ' ')
|
|
43
|
+
.trim()
|
|
44
|
+
.slice(0, 120);
|
|
45
|
+
if (title) {
|
|
46
|
+
items.push({ rank: items.length + 1, title, summary, date, url: url.trim() });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return items;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
const SAMPLE_RSS = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3
|
+
<rss version="2.0"><channel><title>36氪</title>
|
|
4
|
+
<item>
|
|
5
|
+
<title>红杉中国领投AI公司「示例」,金额近2亿元</title>
|
|
6
|
+
<link><![CDATA[https://36kr.com/p/1111111111111111?f=rss]]></link>
|
|
7
|
+
<pubDate>2026-03-26 10:00:00 +0800</pubDate>
|
|
8
|
+
</item>
|
|
9
|
+
<item>
|
|
10
|
+
<title>马斯克旗下xAI估值突破1000亿美元</title>
|
|
11
|
+
<link><![CDATA[https://36kr.com/p/2222222222222222?f=rss]]></link>
|
|
12
|
+
<pubDate>2026-03-26 09:00:00 +0800</pubDate>
|
|
13
|
+
</item>
|
|
14
|
+
<item>
|
|
15
|
+
<title>OpenAI发布GPT-5,多模态能力大幅提升</title>
|
|
16
|
+
<link><![CDATA[https://36kr.com/p/3333333333333333?f=rss]]></link>
|
|
17
|
+
<pubDate>2026-03-25 20:00:00 +0800</pubDate>
|
|
18
|
+
</item>
|
|
19
|
+
</channel></rss>`;
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
describe('36kr/news RSS parsing', () => {
|
|
24
|
+
it('parses RSS feed into ranked news items', async () => {
|
|
25
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
|
26
|
+
ok: true,
|
|
27
|
+
text: async () => SAMPLE_RSS,
|
|
28
|
+
});
|
|
29
|
+
// Direct RSS parse test using the same regex logic as news.ts
|
|
30
|
+
const xml = SAMPLE_RSS;
|
|
31
|
+
const items = [];
|
|
32
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
33
|
+
let match;
|
|
34
|
+
while ((match = itemRegex.exec(xml)) && items.length < 10) {
|
|
35
|
+
const block = match[1];
|
|
36
|
+
const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? '';
|
|
37
|
+
const url = block.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1] ??
|
|
38
|
+
block.match(/<link>(.*?)<\/link>/)?.[1] ??
|
|
39
|
+
'';
|
|
40
|
+
const pubDate = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() ?? '';
|
|
41
|
+
const date = pubDate.slice(0, 10);
|
|
42
|
+
if (title)
|
|
43
|
+
items.push({ rank: items.length + 1, title, date, url: url.trim() });
|
|
44
|
+
}
|
|
45
|
+
expect(items).toHaveLength(3);
|
|
46
|
+
expect(items[0].rank).toBe(1);
|
|
47
|
+
expect(items[0].title).toBe('红杉中国领投AI公司「示例」,金额近2亿元');
|
|
48
|
+
expect(items[0].date).toBe('2026-03-26');
|
|
49
|
+
expect(items[0].url).toBe('https://36kr.com/p/1111111111111111?f=rss');
|
|
50
|
+
});
|
|
51
|
+
it('respects limit — returns at most N items', async () => {
|
|
52
|
+
const xml = SAMPLE_RSS;
|
|
53
|
+
const limit = 2;
|
|
54
|
+
const items = [];
|
|
55
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
56
|
+
let match;
|
|
57
|
+
while ((match = itemRegex.exec(xml)) && items.length < limit) {
|
|
58
|
+
const block = match[1];
|
|
59
|
+
const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? '';
|
|
60
|
+
const url = block.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1] ?? '';
|
|
61
|
+
const pubDate = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() ?? '';
|
|
62
|
+
const date = pubDate.slice(0, 10);
|
|
63
|
+
if (title)
|
|
64
|
+
items.push({ rank: items.length + 1, title, date, url: url.trim() });
|
|
65
|
+
}
|
|
66
|
+
expect(items).toHaveLength(2);
|
|
67
|
+
});
|
|
68
|
+
it('skips items with empty title', async () => {
|
|
69
|
+
const xml = `<rss><channel>
|
|
70
|
+
<item><title></title><link>https://36kr.com/p/0</link><pubDate>2026-01-01</pubDate></item>
|
|
71
|
+
<item><title>有标题的文章</title><link>https://36kr.com/p/1</link><pubDate>2026-01-01</pubDate></item>
|
|
72
|
+
</channel></rss>`;
|
|
73
|
+
const items = [];
|
|
74
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
75
|
+
let match;
|
|
76
|
+
while ((match = itemRegex.exec(xml))) {
|
|
77
|
+
const block = match[1];
|
|
78
|
+
const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? '';
|
|
79
|
+
if (title)
|
|
80
|
+
items.push({ title });
|
|
81
|
+
}
|
|
82
|
+
expect(items).toHaveLength(1);
|
|
83
|
+
expect(items[0].title).toBe('有标题的文章');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 36kr article search — INTERCEPT strategy.
|
|
3
|
+
*
|
|
4
|
+
* Navigates to the 36kr search results page and scrapes rendered articles.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
cli({
|
|
9
|
+
site: '36kr',
|
|
10
|
+
name: 'search',
|
|
11
|
+
description: '搜索36氪文章',
|
|
12
|
+
domain: 'www.36kr.com',
|
|
13
|
+
strategy: Strategy.INTERCEPT,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "AI", "OpenAI")' },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results (max 50)' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['rank', 'title', 'date', 'url'],
|
|
19
|
+
func: async (page, args) => {
|
|
20
|
+
const count = Math.min(Number(args.limit) || 20, 50);
|
|
21
|
+
const query = encodeURIComponent(String(args.query ?? ''));
|
|
22
|
+
await page.installInterceptor('36kr.com/api');
|
|
23
|
+
await page.goto(`https://www.36kr.com/search/articles/${query}`);
|
|
24
|
+
await page.wait(6);
|
|
25
|
+
const domItems = await page.evaluate(`
|
|
26
|
+
(() => {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const results = [];
|
|
29
|
+
// article-item-title contains the clickable title link
|
|
30
|
+
const titleEls = document.querySelectorAll('.article-item-title a[href*="/p/"], .article-item-title[href*="/p/"]');
|
|
31
|
+
for (const el of titleEls) {
|
|
32
|
+
const href = el.getAttribute('href') || '';
|
|
33
|
+
const title = el.textContent?.trim() || '';
|
|
34
|
+
if (!title || seen.has(href)) continue;
|
|
35
|
+
seen.add(href);
|
|
36
|
+
// Look for date near the article item
|
|
37
|
+
const item = el.closest('[class*="article-item"]') || el.parentElement;
|
|
38
|
+
const dateEl = item?.querySelector('[class*="time"], [class*="date"], time');
|
|
39
|
+
const date = dateEl?.textContent?.trim() || '';
|
|
40
|
+
results.push({
|
|
41
|
+
title,
|
|
42
|
+
url: href.startsWith('http') ? href : 'https://36kr.com' + href,
|
|
43
|
+
date,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Fallback: generic /p/ links with meaningful text
|
|
47
|
+
if (results.length === 0) {
|
|
48
|
+
const links = document.querySelectorAll('a[href*="/p/"]');
|
|
49
|
+
for (const el of links) {
|
|
50
|
+
const href = el.getAttribute('href') || '';
|
|
51
|
+
const title = el.textContent?.trim() || '';
|
|
52
|
+
if (!title || title.length < 8 || seen.has(href) || seen.has(title)) continue;
|
|
53
|
+
seen.add(href);
|
|
54
|
+
seen.add(title);
|
|
55
|
+
results.push({ title, url: href.startsWith('http') ? href : 'https://36kr.com' + href, date: '' });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
})()
|
|
60
|
+
`);
|
|
61
|
+
const items = Array.isArray(domItems) ? domItems : [];
|
|
62
|
+
if (items.length === 0) {
|
|
63
|
+
throw new CliError('NO_DATA', 'No results found', `Try a different query or check your keyword`);
|
|
64
|
+
}
|
|
65
|
+
return items.slice(0, count).map((item, i) => ({
|
|
66
|
+
rank: i + 1,
|
|
67
|
+
title: item.title,
|
|
68
|
+
date: item.date,
|
|
69
|
+
url: item.url,
|
|
70
|
+
}));
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'query', positional: true, required: true, help: 'Search keyword' },
|
|
12
12
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results' },
|
|
13
13
|
],
|
|
14
|
-
columns: ['id', 'title', 'author', 'episodes', 'genre'],
|
|
14
|
+
columns: ['id', 'title', 'author', 'episodes', 'genre', 'url'],
|
|
15
15
|
func: async (_page, args) => {
|
|
16
16
|
const term = encodeURIComponent(args.query);
|
|
17
17
|
const limit = Math.max(1, Math.min(Number(args.limit), 25));
|
|
@@ -24,6 +24,7 @@ cli({
|
|
|
24
24
|
author: p.artistName,
|
|
25
25
|
episodes: p.trackCount ?? '-',
|
|
26
26
|
genre: p.primaryGenreName ?? '-',
|
|
27
|
+
url: p.collectionViewUrl || '',
|
|
27
28
|
}));
|
|
28
29
|
},
|
|
29
30
|
});
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
|
|
12
12
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results (max 25)' },
|
|
13
13
|
],
|
|
14
|
-
columns: ['id', 'title', 'authors', 'published'],
|
|
14
|
+
columns: ['id', 'title', 'authors', 'published', 'url'],
|
|
15
15
|
func: async (_page, args) => {
|
|
16
16
|
const limit = Math.max(1, Math.min(Number(args.limit), 25));
|
|
17
17
|
const query = encodeURIComponent(`all:${args.query}`);
|
|
@@ -19,6 +19,6 @@ cli({
|
|
|
19
19
|
const entries = parseEntries(xml);
|
|
20
20
|
if (!entries.length)
|
|
21
21
|
throw new CliError('NOT_FOUND', 'No papers found', 'Try a different keyword');
|
|
22
|
-
return entries.map(e => ({ id: e.id, title: e.title, authors: e.authors, published: e.published }));
|
|
22
|
+
return entries.map(e => ({ id: e.id, title: e.title, authors: e.authors, published: e.published, url: e.url }));
|
|
23
23
|
},
|
|
24
24
|
});
|
package/dist/clis/bbc/news.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bilibili comments — fetches top-level replies via the official API with WBI signing.
|
|
3
|
+
* Uses the /x/v2/reply/main endpoint which is stable and doesn't depend on DOM structure.
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '../../registry.js';
|
|
6
|
+
import { apiGet } from './utils.js';
|
|
7
|
+
cli({
|
|
8
|
+
site: 'bilibili',
|
|
9
|
+
name: 'comments',
|
|
10
|
+
description: '获取 B站视频评论(使用官方 API + WBI 签名)',
|
|
11
|
+
domain: 'www.bilibili.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'bvid', required: true, positional: true, help: 'Video BV ID (e.g. BV1WtAGzYEBm)' },
|
|
15
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of comments (max 50)' },
|
|
16
|
+
],
|
|
17
|
+
columns: ['rank', 'author', 'text', 'likes', 'replies', 'time'],
|
|
18
|
+
func: async (page, kwargs) => {
|
|
19
|
+
const bvid = String(kwargs.bvid).trim();
|
|
20
|
+
const limit = Math.min(Number(kwargs.limit) || 20, 50);
|
|
21
|
+
// Resolve bvid → aid (required by reply API)
|
|
22
|
+
const view = await apiGet(page, '/x/web-interface/view', { params: { bvid } });
|
|
23
|
+
const aid = view?.data?.aid;
|
|
24
|
+
if (!aid)
|
|
25
|
+
throw new Error(`Cannot resolve aid for bvid: ${bvid}`);
|
|
26
|
+
const payload = await apiGet(page, '/x/v2/reply/main', {
|
|
27
|
+
params: { oid: aid, type: 1, mode: 3, ps: limit },
|
|
28
|
+
signed: true,
|
|
29
|
+
});
|
|
30
|
+
const replies = payload?.data?.replies ?? [];
|
|
31
|
+
return replies.slice(0, limit).map((r, i) => ({
|
|
32
|
+
rank: i + 1,
|
|
33
|
+
author: r.member?.uname ?? '',
|
|
34
|
+
text: (r.content?.message ?? '').replace(/\n/g, ' ').trim(),
|
|
35
|
+
likes: r.like ?? 0,
|
|
36
|
+
replies: r.rcount ?? 0,
|
|
37
|
+
time: new Date(r.ctime * 1000).toISOString().slice(0, 16).replace('T', ' '),
|
|
38
|
+
}));
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './comments.js';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockApiGet } = vi.hoisted(() => ({
|
|
3
|
+
mockApiGet: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('./utils.js', () => ({
|
|
6
|
+
apiGet: mockApiGet,
|
|
7
|
+
}));
|
|
8
|
+
import { getRegistry } from '../../registry.js';
|
|
9
|
+
import './comments.js';
|
|
10
|
+
describe('bilibili comments', () => {
|
|
11
|
+
const command = getRegistry().get('bilibili/comments');
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockApiGet.mockReset();
|
|
14
|
+
});
|
|
15
|
+
it('resolves bvid to aid and fetches replies', async () => {
|
|
16
|
+
mockApiGet
|
|
17
|
+
.mockResolvedValueOnce({ data: { aid: 12345 } }) // view endpoint
|
|
18
|
+
.mockResolvedValueOnce({
|
|
19
|
+
data: {
|
|
20
|
+
replies: [
|
|
21
|
+
{
|
|
22
|
+
member: { uname: 'Alice' },
|
|
23
|
+
content: { message: 'Great video!' },
|
|
24
|
+
like: 42,
|
|
25
|
+
rcount: 3,
|
|
26
|
+
ctime: 1700000000,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const result = await command.func({}, { bvid: 'BV1WtAGzYEBm', limit: 5 });
|
|
32
|
+
expect(mockApiGet).toHaveBeenNthCalledWith(1, {}, '/x/web-interface/view', { params: { bvid: 'BV1WtAGzYEBm' } });
|
|
33
|
+
expect(mockApiGet).toHaveBeenNthCalledWith(2, {}, '/x/v2/reply/main', {
|
|
34
|
+
params: { oid: 12345, type: 1, mode: 3, ps: 5 },
|
|
35
|
+
signed: true,
|
|
36
|
+
});
|
|
37
|
+
expect(result).toEqual([
|
|
38
|
+
{
|
|
39
|
+
rank: 1,
|
|
40
|
+
author: 'Alice',
|
|
41
|
+
text: 'Great video!',
|
|
42
|
+
likes: 42,
|
|
43
|
+
replies: 3,
|
|
44
|
+
time: new Date(1700000000 * 1000).toISOString().slice(0, 16).replace('T', ' '),
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
it('throws when aid cannot be resolved', async () => {
|
|
49
|
+
mockApiGet.mockResolvedValueOnce({ data: {} }); // no aid
|
|
50
|
+
await expect(command.func({}, { bvid: 'BV_invalid', limit: 5 })).rejects.toThrow('Cannot resolve aid for bvid: BV_invalid');
|
|
51
|
+
});
|
|
52
|
+
it('returns empty array when replies is missing', async () => {
|
|
53
|
+
mockApiGet
|
|
54
|
+
.mockResolvedValueOnce({ data: { aid: 99 } })
|
|
55
|
+
.mockResolvedValueOnce({ data: {} }); // no replies key
|
|
56
|
+
const result = await command.func({}, { bvid: 'BV1xxx', limit: 5 });
|
|
57
|
+
expect(result).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it('caps limit at 50', async () => {
|
|
60
|
+
mockApiGet
|
|
61
|
+
.mockResolvedValueOnce({ data: { aid: 1 } })
|
|
62
|
+
.mockResolvedValueOnce({ data: { replies: [] } });
|
|
63
|
+
await command.func({}, { bvid: 'BV1xxx', limit: 999 });
|
|
64
|
+
expect(mockApiGet).toHaveBeenNthCalledWith(2, {}, '/x/v2/reply/main', {
|
|
65
|
+
params: { oid: 1, type: 1, mode: 3, ps: 50 },
|
|
66
|
+
signed: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it('collapses newlines in comment text', async () => {
|
|
70
|
+
mockApiGet
|
|
71
|
+
.mockResolvedValueOnce({ data: { aid: 1 } })
|
|
72
|
+
.mockResolvedValueOnce({
|
|
73
|
+
data: {
|
|
74
|
+
replies: [
|
|
75
|
+
{ member: { uname: 'Bob' }, content: { message: 'line1\nline2\nline3' }, like: 0, rcount: 0, ctime: 0 },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const result = (await command.func({}, { bvid: 'BV1xxx', limit: 5 }));
|
|
80
|
+
expect(result[0].text).toBe('line1 line2 line3');
|
|
81
|
+
});
|
|
82
|
+
});
|