@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
|
@@ -54,6 +54,113 @@ export function normalizeWechatUrl(raw: string): string {
|
|
|
54
54
|
return s;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Format a WeChat article timestamp as a UTC+8 datetime string.
|
|
59
|
+
* Accepts either Unix seconds or milliseconds.
|
|
60
|
+
*/
|
|
61
|
+
export function formatWechatTimestamp(rawTimestamp: string): string {
|
|
62
|
+
const ts = Number.parseInt(rawTimestamp, 10);
|
|
63
|
+
if (!Number.isFinite(ts) || ts <= 0) return '';
|
|
64
|
+
|
|
65
|
+
const timestampMs = rawTimestamp.length === 13 ? ts : ts * 1000;
|
|
66
|
+
const d = new Date(timestampMs);
|
|
67
|
+
const pad = (n: number): string => String(n).padStart(2, '0');
|
|
68
|
+
const utc8 = new Date(d.getTime() + 8 * 3600 * 1000);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
`${utc8.getUTCFullYear()}-` +
|
|
72
|
+
`${pad(utc8.getUTCMonth() + 1)}-` +
|
|
73
|
+
`${pad(utc8.getUTCDate())} ` +
|
|
74
|
+
`${pad(utc8.getUTCHours())}:` +
|
|
75
|
+
`${pad(utc8.getUTCMinutes())}:` +
|
|
76
|
+
`${pad(utc8.getUTCSeconds())}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract the raw create_time value from supported WeChat inline script formats.
|
|
82
|
+
*/
|
|
83
|
+
export function extractWechatCreateTimeValue(htmlStr: string): string {
|
|
84
|
+
const jsDecodeMatch = htmlStr.match(
|
|
85
|
+
/create_time\s*:\s*JsDecode\('([^']+)'\)(?=[\s,;}]|$)/,
|
|
86
|
+
);
|
|
87
|
+
if (jsDecodeMatch) return jsDecodeMatch[1];
|
|
88
|
+
|
|
89
|
+
const directValueMatch = htmlStr.match(
|
|
90
|
+
/create_time\s*[:=]\s*(?:"([^"]+)"|'([^']+)'|([0-9A-Za-z]+))(?=[\s,;}]|$)/,
|
|
91
|
+
);
|
|
92
|
+
if (!directValueMatch) return '';
|
|
93
|
+
|
|
94
|
+
return directValueMatch[1] || directValueMatch[2] || directValueMatch[3] || '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract the publish time from DOM text first, then fall back to numeric create_time values.
|
|
99
|
+
*/
|
|
100
|
+
export function extractWechatPublishTime(
|
|
101
|
+
publishTimeText: string | null | undefined,
|
|
102
|
+
htmlStr: string,
|
|
103
|
+
): string {
|
|
104
|
+
const normalizedPublishTime = (publishTimeText || '').trim();
|
|
105
|
+
if (normalizedPublishTime) return normalizedPublishTime;
|
|
106
|
+
|
|
107
|
+
const rawCreateTime = extractWechatCreateTimeValue(htmlStr);
|
|
108
|
+
if (!/^\d{10}$|^\d{13}$/.test(rawCreateTime)) return '';
|
|
109
|
+
|
|
110
|
+
return formatWechatTimestamp(rawCreateTime);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build a self-contained helper for execution inside page.evaluate().
|
|
115
|
+
*/
|
|
116
|
+
export function buildExtractWechatPublishTimeJs(): string {
|
|
117
|
+
return `(${function extractWechatPublishTimeInPage(
|
|
118
|
+
publishTimeText: string | null | undefined,
|
|
119
|
+
htmlStr: string,
|
|
120
|
+
) {
|
|
121
|
+
function formatWechatTimestamp(rawTimestamp: string) {
|
|
122
|
+
const ts = Number.parseInt(rawTimestamp, 10);
|
|
123
|
+
if (!Number.isFinite(ts) || ts <= 0) return '';
|
|
124
|
+
|
|
125
|
+
const timestampMs = rawTimestamp.length === 13 ? ts : ts * 1000;
|
|
126
|
+
const d = new Date(timestampMs);
|
|
127
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
128
|
+
const utc8 = new Date(d.getTime() + 8 * 3600 * 1000);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
`${utc8.getUTCFullYear()}-` +
|
|
132
|
+
`${pad(utc8.getUTCMonth() + 1)}-` +
|
|
133
|
+
`${pad(utc8.getUTCDate())} ` +
|
|
134
|
+
`${pad(utc8.getUTCHours())}:` +
|
|
135
|
+
`${pad(utc8.getUTCMinutes())}:` +
|
|
136
|
+
`${pad(utc8.getUTCSeconds())}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function extractWechatCreateTimeValue(html: string) {
|
|
141
|
+
const jsDecodeMatch = html.match(
|
|
142
|
+
/create_time\s*:\s*JsDecode\('([^']+)'\)(?=[\s,;}]|$)/,
|
|
143
|
+
);
|
|
144
|
+
if (jsDecodeMatch) return jsDecodeMatch[1];
|
|
145
|
+
|
|
146
|
+
const directValueMatch = html.match(
|
|
147
|
+
/create_time\s*[:=]\s*(?:"([^"]+)"|'([^']+)'|([0-9A-Za-z]+))(?=[\s,;}]|$)/,
|
|
148
|
+
);
|
|
149
|
+
if (!directValueMatch) return '';
|
|
150
|
+
|
|
151
|
+
return directValueMatch[1] || directValueMatch[2] || directValueMatch[3] || '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const normalizedPublishTime = (publishTimeText || '').trim();
|
|
155
|
+
if (normalizedPublishTime) return normalizedPublishTime;
|
|
156
|
+
|
|
157
|
+
const rawCreateTime = extractWechatCreateTimeValue(htmlStr);
|
|
158
|
+
if (!/^\d{10}$|^\d{13}$/.test(rawCreateTime)) return '';
|
|
159
|
+
|
|
160
|
+
return formatWechatTimestamp(rawCreateTime);
|
|
161
|
+
}.toString()})`;
|
|
162
|
+
}
|
|
163
|
+
|
|
57
164
|
// ============================================================
|
|
58
165
|
// CLI Registration
|
|
59
166
|
// ============================================================
|
|
@@ -102,26 +209,13 @@ cli({
|
|
|
102
209
|
const authorEl = document.querySelector('#js_name');
|
|
103
210
|
result.author = authorEl ? authorEl.textContent.trim() : '';
|
|
104
211
|
|
|
105
|
-
// Publish time:
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (ts > 0) {
|
|
113
|
-
const d = new Date(ts * 1000);
|
|
114
|
-
const pad = n => String(n).padStart(2, '0');
|
|
115
|
-
const utc8 = new Date(d.getTime() + 8 * 3600 * 1000);
|
|
116
|
-
result.publishTime =
|
|
117
|
-
utc8.getUTCFullYear() + '-' +
|
|
118
|
-
pad(utc8.getUTCMonth() + 1) + '-' +
|
|
119
|
-
pad(utc8.getUTCDate()) + ' ' +
|
|
120
|
-
pad(utc8.getUTCHours()) + ':' +
|
|
121
|
-
pad(utc8.getUTCMinutes()) + ':' +
|
|
122
|
-
pad(utc8.getUTCSeconds());
|
|
123
|
-
}
|
|
124
|
-
}
|
|
212
|
+
// Publish time: prefer the rendered DOM text, then fall back to numeric create_time values.
|
|
213
|
+
const publishTimeEl = document.querySelector('#publish_time');
|
|
214
|
+
const extractWechatPublishTime = ${buildExtractWechatPublishTimeJs()};
|
|
215
|
+
result.publishTime = extractWechatPublishTime(
|
|
216
|
+
publishTimeEl ? publishTimeEl.textContent : '',
|
|
217
|
+
document.documentElement.innerHTML,
|
|
218
|
+
);
|
|
125
219
|
|
|
126
220
|
// Content processing
|
|
127
221
|
const contentEl = document.querySelector('#js_content');
|
package/src/clis/weread/book.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchPrivateApi } from './utils.js';
|
|
4
4
|
|
|
5
5
|
cli({
|
|
6
6
|
site: 'weread',
|
|
@@ -13,7 +13,7 @@ cli({
|
|
|
13
13
|
],
|
|
14
14
|
columns: ['title', 'author', 'publisher', 'intro', 'category', 'rating'],
|
|
15
15
|
func: async (page: IPage, args) => {
|
|
16
|
-
const data = await
|
|
16
|
+
const data = await fetchPrivateApi(page, '/book/info', { bookId: args['book-id'] });
|
|
17
17
|
// newRating is 0-1000 scale per community docs; needs runtime verification
|
|
18
18
|
const rating = data.newRating ? `${(data.newRating / 10).toFixed(1)}%` : '-';
|
|
19
19
|
return [{
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { mockFetchPrivateApi } = vi.hoisted(() => ({
|
|
4
|
+
mockFetchPrivateApi: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('./utils.js', async () => {
|
|
8
|
+
const actual = await vi.importActual<typeof import('./utils.js')>('./utils.js');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
fetchPrivateApi: mockFetchPrivateApi,
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
import { getRegistry } from '../../registry.js';
|
|
16
|
+
import './book.js';
|
|
17
|
+
import './highlights.js';
|
|
18
|
+
import './notes.js';
|
|
19
|
+
|
|
20
|
+
describe('weread book-id positional args', () => {
|
|
21
|
+
const book = getRegistry().get('weread/book');
|
|
22
|
+
const highlights = getRegistry().get('weread/highlights');
|
|
23
|
+
const notes = getRegistry().get('weread/notes');
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mockFetchPrivateApi.mockReset();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('passes the positional book-id to book details', async () => {
|
|
30
|
+
mockFetchPrivateApi.mockResolvedValue({ title: 'Three Body', newRating: 880 });
|
|
31
|
+
|
|
32
|
+
await book!.func!({} as any, { 'book-id': '12345' });
|
|
33
|
+
|
|
34
|
+
expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/book/info', { bookId: '12345' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('passes the positional book-id to highlights', async () => {
|
|
38
|
+
mockFetchPrivateApi.mockResolvedValue({ updated: [] });
|
|
39
|
+
|
|
40
|
+
await highlights!.func!({} as any, { 'book-id': 'abc', limit: 5 });
|
|
41
|
+
|
|
42
|
+
expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/book/bookmarklist', { bookId: 'abc' });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('passes the positional book-id to notes', async () => {
|
|
46
|
+
mockFetchPrivateApi.mockResolvedValue({ reviews: [] });
|
|
47
|
+
|
|
48
|
+
await notes!.func!({} as any, { 'book-id': 'xyz', limit: 5 });
|
|
49
|
+
|
|
50
|
+
expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/review/list', {
|
|
51
|
+
bookId: 'xyz',
|
|
52
|
+
listType: '11',
|
|
53
|
+
mine: '1',
|
|
54
|
+
synckey: '0',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchPrivateApi, formatDate } from './utils.js';
|
|
4
4
|
|
|
5
5
|
cli({
|
|
6
6
|
site: 'weread',
|
|
@@ -14,7 +14,7 @@ cli({
|
|
|
14
14
|
],
|
|
15
15
|
columns: ['chapter', 'text', 'createTime'],
|
|
16
16
|
func: async (page: IPage, args) => {
|
|
17
|
-
const data = await
|
|
17
|
+
const data = await fetchPrivateApi(page, '/book/bookmarklist', { bookId: args['book-id'] });
|
|
18
18
|
const items: any[] = data?.updated ?? [];
|
|
19
19
|
return items.slice(0, Number(args.limit)).map((item: any) => ({
|
|
20
20
|
chapter: item.chapterName ?? '',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchPrivateApi } from './utils.js';
|
|
4
4
|
|
|
5
5
|
cli({
|
|
6
6
|
site: 'weread',
|
|
@@ -10,7 +10,7 @@ cli({
|
|
|
10
10
|
strategy: Strategy.COOKIE,
|
|
11
11
|
columns: ['title', 'author', 'noteCount', 'bookId'],
|
|
12
12
|
func: async (page: IPage, _args) => {
|
|
13
|
-
const data = await
|
|
13
|
+
const data = await fetchPrivateApi(page, '/user/notebooks');
|
|
14
14
|
const books: any[] = data?.books ?? [];
|
|
15
15
|
return books.map((item: any) => ({
|
|
16
16
|
title: item.book?.title ?? '',
|
package/src/clis/weread/notes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchPrivateApi, formatDate } from './utils.js';
|
|
4
4
|
|
|
5
5
|
cli({
|
|
6
6
|
site: 'weread',
|
|
@@ -14,8 +14,8 @@ cli({
|
|
|
14
14
|
],
|
|
15
15
|
columns: ['chapter', 'text', 'review', 'createTime'],
|
|
16
16
|
func: async (page: IPage, args) => {
|
|
17
|
-
const data = await
|
|
18
|
-
bookId: args
|
|
17
|
+
const data = await fetchPrivateApi(page, '/review/list', {
|
|
18
|
+
bookId: args['book-id'],
|
|
19
19
|
listType: '11',
|
|
20
20
|
mine: '1',
|
|
21
21
|
synckey: '0',
|
|
@@ -12,15 +12,16 @@ cli({
|
|
|
12
12
|
{ name: 'query', positional: true, required: true, help: 'Search keyword' },
|
|
13
13
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results' },
|
|
14
14
|
],
|
|
15
|
-
columns: ['rank', 'title', 'author', 'bookId'],
|
|
15
|
+
columns: ['rank', 'title', 'author', 'bookId', 'url'],
|
|
16
16
|
func: async (_page, args) => {
|
|
17
|
-
const data = await fetchWebApi('/search/global', { keyword: args.
|
|
17
|
+
const data = await fetchWebApi('/search/global', { keyword: args.query });
|
|
18
18
|
const books: any[] = data?.books ?? [];
|
|
19
19
|
return books.slice(0, Number(args.limit)).map((item: any, i: number) => ({
|
|
20
20
|
rank: i + 1,
|
|
21
21
|
title: item.bookInfo?.title ?? '',
|
|
22
22
|
author: item.bookInfo?.author ?? '',
|
|
23
23
|
bookId: item.bookInfo?.bookId ?? '',
|
|
24
|
+
url: item.bookInfo?.bookId ? 'https://weread.qq.com/web/bookDetail/' + item.bookInfo.bookId : '',
|
|
24
25
|
}));
|
|
25
26
|
},
|
|
26
27
|
});
|
package/src/clis/weread/shelf.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchPrivateApi } from './utils.js';
|
|
4
4
|
|
|
5
5
|
cli({
|
|
6
6
|
site: 'weread',
|
|
@@ -13,7 +13,7 @@ cli({
|
|
|
13
13
|
],
|
|
14
14
|
columns: ['title', 'author', 'progress', 'bookId'],
|
|
15
15
|
func: async (page: IPage, args) => {
|
|
16
|
-
const data = await
|
|
16
|
+
const data = await fetchPrivateApi(page, '/shelf/sync', { synckey: '0', lectureSynckey: '0' });
|
|
17
17
|
const books: any[] = data?.books ?? [];
|
|
18
18
|
return books.slice(0, Number(args.limit)).map((item: any) => ({
|
|
19
19
|
title: item.bookInfo?.title ?? item.title ?? '',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { formatDate, fetchWebApi
|
|
2
|
+
import { formatDate, fetchWebApi } from './utils.js';
|
|
3
3
|
|
|
4
4
|
describe('formatDate', () => {
|
|
5
5
|
it('formats a typical Unix timestamp in UTC+8', () => {
|
|
@@ -71,34 +71,3 @@ describe('fetchWebApi', () => {
|
|
|
71
71
|
await expect(fetchWebApi('/search/global')).rejects.toThrow('Invalid JSON');
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
|
-
|
|
75
|
-
describe('fetchWithPage', () => {
|
|
76
|
-
it('throws AUTH_REQUIRED on errcode -2010', async () => {
|
|
77
|
-
const mockPage = {
|
|
78
|
-
evaluate: vi.fn().mockResolvedValue({ errcode: -2010, errmsg: '用户不存在' }),
|
|
79
|
-
} as any;
|
|
80
|
-
await expect(fetchWithPage(mockPage, '/book/info')).rejects.toThrow('Not logged in');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('throws API_ERROR on unknown errcode', async () => {
|
|
84
|
-
const mockPage = {
|
|
85
|
-
evaluate: vi.fn().mockResolvedValue({ errcode: -1, errmsg: 'unknown error' }),
|
|
86
|
-
} as any;
|
|
87
|
-
await expect(fetchWithPage(mockPage, '/book/info')).rejects.toThrow('unknown error');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('returns data on success (errcode 0 or absent)', async () => {
|
|
91
|
-
const mockPage = {
|
|
92
|
-
evaluate: vi.fn().mockResolvedValue({ title: 'Test Book', errcode: 0 }),
|
|
93
|
-
} as any;
|
|
94
|
-
const result = await fetchWithPage(mockPage, '/book/info');
|
|
95
|
-
expect(result.title).toBe('Test Book');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('throws FETCH_ERROR on HTTP error', async () => {
|
|
99
|
-
const mockPage = {
|
|
100
|
-
evaluate: vi.fn().mockResolvedValue({ _httpError: '403' }),
|
|
101
|
-
} as any;
|
|
102
|
-
await expect(fetchWithPage(mockPage, '/book/info')).rejects.toThrow('HTTP 403');
|
|
103
|
-
});
|
|
104
|
-
});
|
package/src/clis/weread/utils.ts
CHANGED
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two API domains:
|
|
5
5
|
* - WEB_API (weread.qq.com/web/*): public, Node.js fetch
|
|
6
|
-
* - API (i.weread.qq.com/*): private,
|
|
6
|
+
* - API (i.weread.qq.com/*): private, Node.js fetch with cookies from browser
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { CliError } from '../../errors.js';
|
|
10
|
-
import type { IPage } from '../../types.js';
|
|
10
|
+
import type { BrowserCookie, IPage } from '../../types.js';
|
|
11
11
|
|
|
12
12
|
const WEB_API = 'https://weread.qq.com/web';
|
|
13
13
|
const API = 'https://i.weread.qq.com';
|
|
14
14
|
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
15
15
|
|
|
16
|
+
function buildCookieHeader(cookies: BrowserCookie[]): string {
|
|
17
|
+
return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
21
|
* Fetch a public WeRead web endpoint (Node.js direct fetch).
|
|
18
22
|
* Used by search and ranking commands (browser: false).
|
|
@@ -36,29 +40,50 @@ export async function fetchWebApi(path: string, params?: Record<string, string>)
|
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
|
-
* Fetch a private WeRead API endpoint
|
|
40
|
-
*
|
|
43
|
+
* Fetch a private WeRead API endpoint with cookies extracted from the browser.
|
|
44
|
+
* The HTTP request itself runs in Node.js to avoid page-context CORS failures.
|
|
41
45
|
*/
|
|
42
|
-
export async function
|
|
46
|
+
export async function fetchPrivateApi(page: IPage, path: string, params?: Record<string, string>): Promise<any> {
|
|
43
47
|
const url = new URL(`${API}${path}`);
|
|
44
48
|
if (params) {
|
|
45
49
|
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
46
50
|
}
|
|
47
51
|
const urlStr = url.toString();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
|
|
53
|
+
const cookies = await page.getCookies({ url: urlStr });
|
|
54
|
+
const cookieHeader = buildCookieHeader(cookies);
|
|
55
|
+
|
|
56
|
+
let resp: Response;
|
|
57
|
+
try {
|
|
58
|
+
resp = await fetch(urlStr, {
|
|
59
|
+
headers: {
|
|
60
|
+
'User-Agent': UA,
|
|
61
|
+
'Origin': 'https://weread.qq.com',
|
|
62
|
+
'Referer': 'https://weread.qq.com/',
|
|
63
|
+
...(cookieHeader ? { 'Cookie': cookieHeader } : {}),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new CliError(
|
|
68
|
+
'FETCH_ERROR',
|
|
69
|
+
`Failed to fetch ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
70
|
+
'WeRead API may be temporarily unavailable',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let data: any;
|
|
75
|
+
try {
|
|
76
|
+
data = await resp.json();
|
|
77
|
+
} catch {
|
|
78
|
+
throw new CliError('PARSE_ERROR', `Invalid JSON response for ${path}`, 'WeRead may have returned an HTML error page');
|
|
58
79
|
}
|
|
59
|
-
|
|
80
|
+
|
|
81
|
+
if (resp.status === 401 || data?.errcode === -2010) {
|
|
60
82
|
throw new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first');
|
|
61
83
|
}
|
|
84
|
+
if (!resp.ok) {
|
|
85
|
+
throw new CliError('FETCH_ERROR', `HTTP ${resp.status} for ${path}`, 'WeRead API may be temporarily unavailable');
|
|
86
|
+
}
|
|
62
87
|
if (data?.errcode != null && data.errcode !== 0) {
|
|
63
88
|
throw new CliError('API_ERROR', data.errmsg ?? `WeRead API error ${data.errcode}`);
|
|
64
89
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
import { getRegistry } from '../../registry.js';
|
|
4
|
+
import './comments.js';
|
|
5
|
+
|
|
6
|
+
function createPageMock(evaluateResult: any): IPage {
|
|
7
|
+
return {
|
|
8
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
9
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
10
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
16
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
18
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
22
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
23
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
27
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
28
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('xiaohongshu comments', () => {
|
|
33
|
+
const command = getRegistry().get('xiaohongshu/comments');
|
|
34
|
+
|
|
35
|
+
it('returns ranked comment rows', async () => {
|
|
36
|
+
const page = createPageMock({
|
|
37
|
+
loginWall: false,
|
|
38
|
+
results: [
|
|
39
|
+
{ author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01' },
|
|
40
|
+
{ author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02' },
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const result = (await command!.func!(page, { 'note-id': '69aadbcb000000002202f131', limit: 5 })) as any[];
|
|
45
|
+
|
|
46
|
+
expect((page.goto as any).mock.calls[0][0]).toContain('/explore/69aadbcb000000002202f131');
|
|
47
|
+
expect(result).toEqual([
|
|
48
|
+
{ rank: 1, author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01' },
|
|
49
|
+
{ rank: 2, author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02' },
|
|
50
|
+
]);
|
|
51
|
+
expect(result[0]).not.toHaveProperty('loginWall');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('strips /explore/ prefix from full URL input', async () => {
|
|
55
|
+
const page = createPageMock({
|
|
56
|
+
loginWall: false,
|
|
57
|
+
results: [{ author: 'Alice', text: 'Nice', likes: 1, time: '2024-01-01' }],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await command!.func!(page, {
|
|
61
|
+
'note-id': 'https://www.xiaohongshu.com/explore/69aadbcb000000002202f131',
|
|
62
|
+
limit: 5,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect((page.goto as any).mock.calls[0][0]).toContain('/explore/69aadbcb000000002202f131');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('throws AuthRequiredError when login wall is detected', async () => {
|
|
69
|
+
const page = createPageMock({ loginWall: true, results: [] });
|
|
70
|
+
|
|
71
|
+
await expect(command!.func!(page, { 'note-id': 'abc123', limit: 5 })).rejects.toThrow(
|
|
72
|
+
'Note comments require login',
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns empty array when no comments are found', async () => {
|
|
77
|
+
const page = createPageMock({ loginWall: false, results: [] });
|
|
78
|
+
|
|
79
|
+
await expect(command!.func!(page, { 'note-id': 'abc123', limit: 5 })).resolves.toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('respects the limit', async () => {
|
|
83
|
+
const manyComments = Array.from({ length: 10 }, (_, i) => ({
|
|
84
|
+
author: `User${i}`,
|
|
85
|
+
text: `Comment ${i}`,
|
|
86
|
+
likes: i,
|
|
87
|
+
time: '2024-01-01',
|
|
88
|
+
}));
|
|
89
|
+
const page = createPageMock({ loginWall: false, results: manyComments });
|
|
90
|
+
|
|
91
|
+
const result = (await command!.func!(page, { 'note-id': 'abc123', limit: 3 })) as any[];
|
|
92
|
+
expect(result).toHaveLength(3);
|
|
93
|
+
expect(result[0].rank).toBe(1);
|
|
94
|
+
expect(result[2].rank).toBe(3);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaohongshu comments — DOM extraction from note detail page.
|
|
3
|
+
* XHS API requires signed requests, so we scrape the rendered DOM instead.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
8
|
+
|
|
9
|
+
cli({
|
|
10
|
+
site: 'xiaohongshu',
|
|
11
|
+
name: 'comments',
|
|
12
|
+
description: '获取小红书笔记评论(仅主评论,不含楼中楼)',
|
|
13
|
+
domain: 'www.xiaohongshu.com',
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'note-id', required: true, positional: true, help: 'Note ID or full /explore/<id> URL' },
|
|
17
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of comments (max 50)' },
|
|
18
|
+
],
|
|
19
|
+
columns: ['rank', 'author', 'text', 'likes', 'time'],
|
|
20
|
+
func: async (page, kwargs) => {
|
|
21
|
+
const limit = Math.min(Number(kwargs.limit) || 20, 50);
|
|
22
|
+
let noteId = String(kwargs['note-id']).trim();
|
|
23
|
+
|
|
24
|
+
// Accept full URLs: /explore/<id> or /note/<id>
|
|
25
|
+
const urlMatch = noteId.match(/\/explore\/([a-f0-9]+)/) || noteId.match(/\/note\/([a-f0-9]+)/);
|
|
26
|
+
if (urlMatch) noteId = urlMatch[1];
|
|
27
|
+
|
|
28
|
+
await page.goto(`https://www.xiaohongshu.com/explore/${noteId}`);
|
|
29
|
+
await page.wait(3);
|
|
30
|
+
|
|
31
|
+
const data = await page.evaluate(`
|
|
32
|
+
(async () => {
|
|
33
|
+
const wait = (ms) => new Promise(r => setTimeout(r, ms))
|
|
34
|
+
|
|
35
|
+
// Check login state
|
|
36
|
+
const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
|
|
37
|
+
|
|
38
|
+
// Scroll the note container to trigger comment loading
|
|
39
|
+
const scroller = document.querySelector('.note-scroller') || document.querySelector('.container')
|
|
40
|
+
if (scroller) {
|
|
41
|
+
for (let i = 0; i < 3; i++) {
|
|
42
|
+
scroller.scrollTo(0, scroller.scrollHeight)
|
|
43
|
+
await wait(1000)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
|
|
48
|
+
|
|
49
|
+
const results = []
|
|
50
|
+
const parents = document.querySelectorAll('.parent-comment')
|
|
51
|
+
for (const p of parents) {
|
|
52
|
+
const item = p.querySelector('.comment-item')
|
|
53
|
+
if (!item) continue
|
|
54
|
+
|
|
55
|
+
const author = clean(item.querySelector('.author-wrapper .name, .user-name'))
|
|
56
|
+
const text = clean(item.querySelector('.content, .note-text'))
|
|
57
|
+
// XHS shows text "赞" when likes = 0; only shows a number when > 0
|
|
58
|
+
const likesRaw = clean(item.querySelector('.count'))
|
|
59
|
+
const likes = /^\\d+$/.test(likesRaw) ? Number(likesRaw) : 0
|
|
60
|
+
const time = clean(item.querySelector('.date, .time'))
|
|
61
|
+
|
|
62
|
+
if (!text) continue
|
|
63
|
+
results.push({ author, text, likes, time })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { loginWall, results }
|
|
67
|
+
})()
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
if (!data || typeof data !== 'object') {
|
|
71
|
+
throw new EmptyResultError('xiaohongshu/comments', 'Unexpected evaluate response');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if ((data as any).loginWall) {
|
|
75
|
+
throw new AuthRequiredError('www.xiaohongshu.com', 'Note comments require login');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const results: any[] = (data as any).results ?? [];
|
|
79
|
+
return results.slice(0, limit).map((c: any, i: number) => ({ rank: i + 1, ...c }));
|
|
80
|
+
},
|
|
81
|
+
});
|