@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
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Douban adapter utilities.
|
|
3
3
|
*/
|
|
4
|
-
import { CliError } from '../../errors.js';
|
|
4
|
+
import { ArgumentError, CliError, EmptyResultError } from '../../errors.js';
|
|
5
|
+
const DOUBAN_PHOTO_PAGE_SIZE = 30;
|
|
6
|
+
const MAX_DOUBAN_PHOTOS = 500;
|
|
5
7
|
function clampLimit(limit) {
|
|
6
8
|
return Math.max(1, Math.min(limit || 20, 50));
|
|
7
9
|
}
|
|
10
|
+
function clampPhotoLimit(limit) {
|
|
11
|
+
return Math.max(1, Math.min(limit || 120, MAX_DOUBAN_PHOTOS));
|
|
12
|
+
}
|
|
8
13
|
async function ensureDoubanReady(page) {
|
|
9
14
|
const state = await page.evaluate(`
|
|
10
15
|
(() => {
|
|
@@ -18,6 +23,190 @@ async function ensureDoubanReady(page) {
|
|
|
18
23
|
throw new CliError('AUTH_REQUIRED', 'Douban requires a logged-in browser session before these commands can load data.', 'Please sign in to douban.com in the browser that opencli reuses, then rerun the command.');
|
|
19
24
|
}
|
|
20
25
|
}
|
|
26
|
+
export function normalizeDoubanSubjectId(subjectId) {
|
|
27
|
+
const normalized = String(subjectId || '').trim();
|
|
28
|
+
if (!/^\d+$/.test(normalized)) {
|
|
29
|
+
throw new ArgumentError(`Invalid Douban subject ID: ${subjectId}`);
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
export function promoteDoubanPhotoUrl(url, size = 'l') {
|
|
34
|
+
const normalized = String(url || '').trim();
|
|
35
|
+
if (!normalized)
|
|
36
|
+
return '';
|
|
37
|
+
if (/^[a-z]+:/i.test(normalized) && !/^https?:/i.test(normalized))
|
|
38
|
+
return '';
|
|
39
|
+
return normalized.replace(/\/view\/photo\/[^/]+\/public\//, `/view/photo/${size}/public/`);
|
|
40
|
+
}
|
|
41
|
+
export function resolveDoubanPhotoAssetUrl(candidates, baseUrl = '') {
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
const normalized = String(candidate || '').trim();
|
|
44
|
+
if (!normalized)
|
|
45
|
+
continue;
|
|
46
|
+
let resolved = normalized;
|
|
47
|
+
try {
|
|
48
|
+
resolved = baseUrl
|
|
49
|
+
? new URL(normalized, baseUrl).toString()
|
|
50
|
+
: new URL(normalized).toString();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
resolved = normalized;
|
|
54
|
+
}
|
|
55
|
+
if (/^https?:\/\//i.test(resolved)) {
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
export function getDoubanPhotoExtension(url) {
|
|
62
|
+
const normalized = String(url || '').trim();
|
|
63
|
+
if (!normalized)
|
|
64
|
+
return '.jpg';
|
|
65
|
+
try {
|
|
66
|
+
const ext = new URL(normalized).pathname.match(/\.(jpe?g|png|gif|webp|avif|bmp)$/i)?.[0];
|
|
67
|
+
return ext || '.jpg';
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
const ext = normalized.match(/\.(jpe?g|png|gif|webp|avif|bmp)(?:$|[?#])/i)?.[0];
|
|
71
|
+
return ext ? ext.replace(/[?#].*$/, '') : '.jpg';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function loadDoubanSubjectPhotos(page, subjectId, options = {}) {
|
|
75
|
+
const normalizedId = normalizeDoubanSubjectId(subjectId);
|
|
76
|
+
const type = String(options.type || 'Rb').trim() || 'Rb';
|
|
77
|
+
const targetPhotoId = String(options.targetPhotoId || '').trim();
|
|
78
|
+
const safeLimit = targetPhotoId ? Number.MAX_SAFE_INTEGER : clampPhotoLimit(Number(options.limit) || 120);
|
|
79
|
+
const resolvePhotoAssetUrlSource = resolveDoubanPhotoAssetUrl.toString();
|
|
80
|
+
const galleryUrl = `https://movie.douban.com/subject/${normalizedId}/photos?type=${encodeURIComponent(type)}`;
|
|
81
|
+
await page.goto(galleryUrl);
|
|
82
|
+
await page.wait(2);
|
|
83
|
+
await ensureDoubanReady(page);
|
|
84
|
+
const data = await page.evaluate(`
|
|
85
|
+
(async () => {
|
|
86
|
+
const subjectId = ${JSON.stringify(normalizedId)};
|
|
87
|
+
const type = ${JSON.stringify(type)};
|
|
88
|
+
const limit = ${safeLimit};
|
|
89
|
+
const targetPhotoId = ${JSON.stringify(targetPhotoId)};
|
|
90
|
+
const pageSize = ${DOUBAN_PHOTO_PAGE_SIZE};
|
|
91
|
+
const resolveDoubanPhotoAssetUrl = ${resolvePhotoAssetUrlSource};
|
|
92
|
+
|
|
93
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
94
|
+
const toAbsoluteUrl = (value) => {
|
|
95
|
+
if (!value) return '';
|
|
96
|
+
try {
|
|
97
|
+
return new URL(value, location.origin).toString();
|
|
98
|
+
} catch {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const promotePhotoUrl = (value) => {
|
|
103
|
+
const absolute = toAbsoluteUrl(value);
|
|
104
|
+
if (!absolute) return '';
|
|
105
|
+
if (/^[a-z]+:/i.test(absolute) && !/^https?:/i.test(absolute)) return '';
|
|
106
|
+
return absolute.replace(/\\/view\\/photo\\/[^/]+\\/public\\//, '/view/photo/l/public/');
|
|
107
|
+
};
|
|
108
|
+
const buildPageUrl = (start) => {
|
|
109
|
+
const url = new URL(location.href);
|
|
110
|
+
url.searchParams.set('type', type);
|
|
111
|
+
if (start > 0) url.searchParams.set('start', String(start));
|
|
112
|
+
else url.searchParams.delete('start');
|
|
113
|
+
return url.toString();
|
|
114
|
+
};
|
|
115
|
+
const getTitle = (doc) => {
|
|
116
|
+
const raw = normalize(doc.querySelector('#content h1')?.textContent)
|
|
117
|
+
|| normalize(doc.querySelector('title')?.textContent);
|
|
118
|
+
return raw.replace(/\\s*\\(豆瓣\\)\\s*$/, '');
|
|
119
|
+
};
|
|
120
|
+
const extractPhotos = (doc, pageNumber) => {
|
|
121
|
+
const nodes = Array.from(doc.querySelectorAll('.poster-col3 li, .poster-col3l li, .article li'));
|
|
122
|
+
const rows = [];
|
|
123
|
+
for (const node of nodes) {
|
|
124
|
+
const link = node.querySelector('a[href*="/photos/photo/"]');
|
|
125
|
+
const img = node.querySelector('img');
|
|
126
|
+
if (!link || !img) continue;
|
|
127
|
+
|
|
128
|
+
const detailUrl = toAbsoluteUrl(link.getAttribute('href') || '');
|
|
129
|
+
const photoId = detailUrl.match(/\\/photo\\/(\\d+)/)?.[1] || '';
|
|
130
|
+
const thumbUrl = resolveDoubanPhotoAssetUrl([
|
|
131
|
+
img.getAttribute('data-origin'),
|
|
132
|
+
img.getAttribute('data-src'),
|
|
133
|
+
img.getAttribute('src'),
|
|
134
|
+
], location.href);
|
|
135
|
+
const imageUrl = promotePhotoUrl(thumbUrl);
|
|
136
|
+
const title = normalize(link.getAttribute('title'))
|
|
137
|
+
|| normalize(img.getAttribute('alt'))
|
|
138
|
+
|| (photoId ? 'photo_' + photoId : 'photo_' + String(rows.length + 1));
|
|
139
|
+
|
|
140
|
+
if (!detailUrl || !thumbUrl || !imageUrl) continue;
|
|
141
|
+
|
|
142
|
+
rows.push({
|
|
143
|
+
photoId,
|
|
144
|
+
title,
|
|
145
|
+
imageUrl,
|
|
146
|
+
thumbUrl,
|
|
147
|
+
detailUrl,
|
|
148
|
+
page: pageNumber,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return rows;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const subjectTitle = getTitle(document);
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
const photos = [];
|
|
157
|
+
|
|
158
|
+
for (let pageIndex = 0; photos.length < limit; pageIndex += 1) {
|
|
159
|
+
let doc = document;
|
|
160
|
+
if (pageIndex > 0) {
|
|
161
|
+
const response = await fetch(buildPageUrl(pageIndex * pageSize), { credentials: 'include' });
|
|
162
|
+
if (!response.ok) break;
|
|
163
|
+
const html = await response.text();
|
|
164
|
+
doc = new DOMParser().parseFromString(html, 'text/html');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const pagePhotos = extractPhotos(doc, pageIndex + 1);
|
|
168
|
+
if (!pagePhotos.length) break;
|
|
169
|
+
|
|
170
|
+
let appended = 0;
|
|
171
|
+
let foundTarget = false;
|
|
172
|
+
for (const photo of pagePhotos) {
|
|
173
|
+
const key = photo.photoId || photo.detailUrl || photo.imageUrl;
|
|
174
|
+
if (seen.has(key)) continue;
|
|
175
|
+
seen.add(key);
|
|
176
|
+
photos.push({
|
|
177
|
+
index: photos.length + 1,
|
|
178
|
+
...photo,
|
|
179
|
+
});
|
|
180
|
+
appended += 1;
|
|
181
|
+
if (targetPhotoId && photo.photoId === targetPhotoId) {
|
|
182
|
+
foundTarget = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
if (photos.length >= limit) break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (foundTarget || pagePhotos.length < pageSize || appended === 0) break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
subjectId,
|
|
193
|
+
subjectTitle,
|
|
194
|
+
type,
|
|
195
|
+
photos,
|
|
196
|
+
};
|
|
197
|
+
})()
|
|
198
|
+
`);
|
|
199
|
+
const photos = Array.isArray(data?.photos) ? data.photos : [];
|
|
200
|
+
if (!photos.length) {
|
|
201
|
+
throw new EmptyResultError('douban photos', 'No photos found. Try a different subject ID or a different --type value such as Rb.');
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
subjectId: normalizedId,
|
|
205
|
+
subjectTitle: String(data?.subjectTitle || '').trim(),
|
|
206
|
+
type,
|
|
207
|
+
photos,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
21
210
|
export async function loadDoubanBookHot(page, limit) {
|
|
22
211
|
const safeLimit = clampLimit(limit);
|
|
23
212
|
await page.goto('https://book.douban.com/chart');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getDoubanPhotoExtension, loadDoubanSubjectPhotos, normalizeDoubanSubjectId, promoteDoubanPhotoUrl, resolveDoubanPhotoAssetUrl, } from './utils.js';
|
|
3
|
+
describe('douban utils', () => {
|
|
4
|
+
it('normalizes valid subject ids', () => {
|
|
5
|
+
expect(normalizeDoubanSubjectId(' 30382501 ')).toBe('30382501');
|
|
6
|
+
});
|
|
7
|
+
it('rejects invalid subject ids', () => {
|
|
8
|
+
expect(() => normalizeDoubanSubjectId('tt30382501')).toThrow('Invalid Douban subject ID');
|
|
9
|
+
});
|
|
10
|
+
it('promotes thumbnail urls to large photo urls', () => {
|
|
11
|
+
expect(promoteDoubanPhotoUrl('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp')).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp');
|
|
12
|
+
expect(promoteDoubanPhotoUrl('https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2578474613.jpg')).toBe('https://img9.doubanio.com/view/photo/l/public/p2578474613.jpg');
|
|
13
|
+
});
|
|
14
|
+
it('rejects non-http photo urls during promotion', () => {
|
|
15
|
+
expect(promoteDoubanPhotoUrl('data:image/gif;base64,abc')).toBe('');
|
|
16
|
+
});
|
|
17
|
+
it('prefers lazy-loaded photo urls over data placeholders', () => {
|
|
18
|
+
expect(resolveDoubanPhotoAssetUrl([
|
|
19
|
+
'',
|
|
20
|
+
'https://img1.doubanio.com/view/photo/m/public/p2913450214.webp',
|
|
21
|
+
'data:image/gif;base64,abc',
|
|
22
|
+
], 'https://movie.douban.com/subject/30382501/photos?type=Rb')).toBe('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp');
|
|
23
|
+
});
|
|
24
|
+
it('drops unsupported non-http photo urls when no real image url exists', () => {
|
|
25
|
+
expect(resolveDoubanPhotoAssetUrl(['data:image/gif;base64,abc', 'blob:https://movie.douban.com/example'], 'https://movie.douban.com/subject/30382501/photos?type=Rb')).toBe('');
|
|
26
|
+
});
|
|
27
|
+
it('removes the default photo cap when scanning for an exact photo id', async () => {
|
|
28
|
+
const evaluate = vi.fn()
|
|
29
|
+
.mockResolvedValueOnce({ blocked: false, title: 'Some Movie', href: 'https://movie.douban.com/subject/30382501/photos?type=Rb' })
|
|
30
|
+
.mockResolvedValueOnce({
|
|
31
|
+
subjectId: '30382501',
|
|
32
|
+
subjectTitle: 'The Wandering Earth 2',
|
|
33
|
+
type: 'Rb',
|
|
34
|
+
photos: [
|
|
35
|
+
{
|
|
36
|
+
index: 731,
|
|
37
|
+
photoId: '2913450215',
|
|
38
|
+
title: 'Character poster',
|
|
39
|
+
imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
|
|
40
|
+
thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450215.jpg',
|
|
41
|
+
detailUrl: 'https://movie.douban.com/photos/photo/2913450215/',
|
|
42
|
+
page: 25,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
const page = {
|
|
47
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
49
|
+
evaluate,
|
|
50
|
+
};
|
|
51
|
+
await loadDoubanSubjectPhotos(page, '30382501', {
|
|
52
|
+
type: 'Rb',
|
|
53
|
+
targetPhotoId: '2913450215',
|
|
54
|
+
});
|
|
55
|
+
const scanScript = evaluate.mock.calls[1]?.[0];
|
|
56
|
+
expect(scanScript).toContain('const targetPhotoId = "2913450215";');
|
|
57
|
+
expect(scanScript).toContain(`const limit = ${Number.MAX_SAFE_INTEGER};`);
|
|
58
|
+
expect(scanScript).toContain('for (let pageIndex = 0; photos.length < limit; pageIndex += 1)');
|
|
59
|
+
});
|
|
60
|
+
it('keeps image extensions when download urls contain query params', () => {
|
|
61
|
+
expect(getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp?foo=1')).toBe('.webp');
|
|
62
|
+
expect(getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.jpeg')).toBe('.jpeg');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IPage } from '../../../types.js';
|
|
2
|
+
export interface FetchOptions {
|
|
3
|
+
body?: unknown;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Execute a fetch() call inside the Chrome browser context via page.evaluate.
|
|
8
|
+
* This ensures a_bogus signing and cookies are handled automatically by the browser.
|
|
9
|
+
*/
|
|
10
|
+
export declare function browserFetch(page: IPage, method: 'GET' | 'POST', url: string, options?: FetchOptions): Promise<unknown>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CommandExecutionError } from '../../../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Execute a fetch() call inside the Chrome browser context via page.evaluate.
|
|
4
|
+
* This ensures a_bogus signing and cookies are handled automatically by the browser.
|
|
5
|
+
*/
|
|
6
|
+
export async function browserFetch(page, method, url, options = {}) {
|
|
7
|
+
const js = `
|
|
8
|
+
(async () => {
|
|
9
|
+
const res = await fetch(${JSON.stringify(url)}, {
|
|
10
|
+
method: ${JSON.stringify(method)},
|
|
11
|
+
credentials: 'include',
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
...${JSON.stringify(options.headers ?? {})}
|
|
15
|
+
},
|
|
16
|
+
${options.body ? `body: JSON.stringify(${JSON.stringify(options.body)}),` : ''}
|
|
17
|
+
});
|
|
18
|
+
return res.json();
|
|
19
|
+
})()
|
|
20
|
+
`;
|
|
21
|
+
const result = await page.evaluate(js);
|
|
22
|
+
if (result && typeof result === 'object' && 'status_code' in result) {
|
|
23
|
+
const code = result.status_code;
|
|
24
|
+
if (code !== 0) {
|
|
25
|
+
const msg = result.status_msg ?? 'unknown error';
|
|
26
|
+
throw new CommandExecutionError(`Douyin API error ${code}: ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { browserFetch } from './browser-fetch.js';
|
|
3
|
+
function makePage(result) {
|
|
4
|
+
return {
|
|
5
|
+
goto: vi.fn(), evaluate: vi.fn().mockResolvedValue(result),
|
|
6
|
+
getCookies: vi.fn(), snapshot: vi.fn(), click: vi.fn(),
|
|
7
|
+
typeText: vi.fn(), pressKey: vi.fn(), scrollTo: vi.fn(),
|
|
8
|
+
getFormState: vi.fn(), wait: vi.fn(), tabs: vi.fn(),
|
|
9
|
+
closeTab: vi.fn(), newTab: vi.fn(), selectTab: vi.fn(),
|
|
10
|
+
networkRequests: vi.fn(), consoleMessages: vi.fn(),
|
|
11
|
+
scroll: vi.fn(), autoScroll: vi.fn(),
|
|
12
|
+
installInterceptor: vi.fn(), getInterceptedRequests: vi.fn(),
|
|
13
|
+
screenshot: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe('browserFetch', () => {
|
|
17
|
+
it('returns parsed JSON on success', async () => {
|
|
18
|
+
const page = makePage({ status_code: 0, data: { ak: 'KEY' } });
|
|
19
|
+
const result = await browserFetch(page, 'GET', 'https://creator.douyin.com/api/test');
|
|
20
|
+
expect(result).toEqual({ status_code: 0, data: { ak: 'KEY' } });
|
|
21
|
+
});
|
|
22
|
+
it('throws when status_code is non-zero', async () => {
|
|
23
|
+
const page = makePage({ status_code: 8, message: 'fail' });
|
|
24
|
+
await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test')).rejects.toThrow('Douyin API error 8');
|
|
25
|
+
});
|
|
26
|
+
it('returns result even when no status_code field', async () => {
|
|
27
|
+
const page = makePage({ some_field: 'value' });
|
|
28
|
+
const result = await browserFetch(page, 'GET', 'https://creator.douyin.com/api/test');
|
|
29
|
+
expect(result).toEqual({ some_field: 'value' });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateCreationId(): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateCreationId } from './creation-id.js';
|
|
3
|
+
describe('generateCreationId', () => {
|
|
4
|
+
it('starts with "pin"', () => {
|
|
5
|
+
expect(generateCreationId()).toMatch(/^pin/);
|
|
6
|
+
});
|
|
7
|
+
it('has 4 random lowercase-alphanumeric chars after "pin"', () => {
|
|
8
|
+
expect(generateCreationId()).toMatch(/^pin[a-z0-9]{4}/);
|
|
9
|
+
});
|
|
10
|
+
it('ends with a numeric timestamp (ms)', () => {
|
|
11
|
+
const before = Date.now();
|
|
12
|
+
const id = generateCreationId();
|
|
13
|
+
const after = Date.now();
|
|
14
|
+
const ts = parseInt(id.replace(/^pin[a-z0-9]{4}/, ''), 10);
|
|
15
|
+
expect(ts).toBeGreaterThanOrEqual(before);
|
|
16
|
+
expect(ts).toBeLessThanOrEqual(after);
|
|
17
|
+
});
|
|
18
|
+
it('generates unique IDs', () => {
|
|
19
|
+
const ids = new Set(Array.from({ length: 100 }, generateCreationId));
|
|
20
|
+
expect(ids.size).toBe(100);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageX cover image uploader.
|
|
3
|
+
*
|
|
4
|
+
* Uploads a JPEG/PNG image to ByteDance ImageX via a pre-signed PUT URL
|
|
5
|
+
* obtained from the Douyin "apply cover upload" API.
|
|
6
|
+
*/
|
|
7
|
+
export interface ImageXUploadInfo {
|
|
8
|
+
/** Pre-signed PUT target URL (provided by the apply cover upload API) */
|
|
9
|
+
upload_url: string;
|
|
10
|
+
/** Image URI to use in create_v2 (returned from the apply step) */
|
|
11
|
+
store_uri: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Upload a cover image to ByteDance ImageX via a pre-signed PUT URL.
|
|
15
|
+
*
|
|
16
|
+
* @param imagePath - Local file path to the image (JPEG/PNG/etc.)
|
|
17
|
+
* @param uploadInfo - Upload URL and store_uri from the apply cover upload API
|
|
18
|
+
* @returns The store_uri (= image_uri for use in create_v2)
|
|
19
|
+
*/
|
|
20
|
+
export declare function imagexUpload(imagePath: string, uploadInfo: ImageXUploadInfo): Promise<string>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageX cover image uploader.
|
|
3
|
+
*
|
|
4
|
+
* Uploads a JPEG/PNG image to ByteDance ImageX via a pre-signed PUT URL
|
|
5
|
+
* obtained from the Douyin "apply cover upload" API.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { CommandExecutionError } from '../../../errors.js';
|
|
10
|
+
/**
|
|
11
|
+
* Detect MIME type from file extension.
|
|
12
|
+
* Falls back to image/jpeg for unknown extensions.
|
|
13
|
+
*/
|
|
14
|
+
function detectContentType(filePath) {
|
|
15
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
16
|
+
switch (ext) {
|
|
17
|
+
case '.png':
|
|
18
|
+
return 'image/png';
|
|
19
|
+
case '.gif':
|
|
20
|
+
return 'image/gif';
|
|
21
|
+
case '.webp':
|
|
22
|
+
return 'image/webp';
|
|
23
|
+
default:
|
|
24
|
+
return 'image/jpeg';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Upload a cover image to ByteDance ImageX via a pre-signed PUT URL.
|
|
29
|
+
*
|
|
30
|
+
* @param imagePath - Local file path to the image (JPEG/PNG/etc.)
|
|
31
|
+
* @param uploadInfo - Upload URL and store_uri from the apply cover upload API
|
|
32
|
+
* @returns The store_uri (= image_uri for use in create_v2)
|
|
33
|
+
*/
|
|
34
|
+
export async function imagexUpload(imagePath, uploadInfo) {
|
|
35
|
+
if (!fs.existsSync(imagePath)) {
|
|
36
|
+
throw new CommandExecutionError(`Cover image file not found: ${imagePath}`, 'Ensure the file path is correct and accessible.');
|
|
37
|
+
}
|
|
38
|
+
const imageBuffer = fs.readFileSync(imagePath);
|
|
39
|
+
const contentType = detectContentType(imagePath);
|
|
40
|
+
const res = await fetch(uploadInfo.upload_url, {
|
|
41
|
+
method: 'PUT',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': contentType,
|
|
44
|
+
'Content-Length': String(imageBuffer.byteLength),
|
|
45
|
+
},
|
|
46
|
+
body: imageBuffer,
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const body = await res.text().catch(() => '');
|
|
50
|
+
throw new CommandExecutionError(`ImageX upload failed with status ${res.status}: ${body}`, 'Check that the upload URL is valid and has not expired.');
|
|
51
|
+
}
|
|
52
|
+
return uploadInfo.store_uri;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { CommandExecutionError } from '../../../errors.js';
|
|
6
|
+
import { imagexUpload } from './imagex-upload.js';
|
|
7
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
8
|
+
function makeTempImage(ext = '.jpg') {
|
|
9
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'imagex-test-'));
|
|
10
|
+
const filePath = path.join(dir, `cover${ext}`);
|
|
11
|
+
fs.writeFileSync(filePath, Buffer.from([0xff, 0xd8, 0xff, 0xe0])); // minimal JPEG header bytes
|
|
12
|
+
return filePath;
|
|
13
|
+
}
|
|
14
|
+
const FAKE_UPLOAD_INFO = {
|
|
15
|
+
upload_url: 'https://imagex.bytedance.com/upload/presigned/fake',
|
|
16
|
+
store_uri: 'tos-cn-i-alisg.example.com/cover/abc123',
|
|
17
|
+
};
|
|
18
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
19
|
+
describe('imagexUpload', () => {
|
|
20
|
+
let imagePath;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
imagePath = makeTempImage('.jpg');
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
// Clean up temp files
|
|
26
|
+
try {
|
|
27
|
+
fs.unlinkSync(imagePath);
|
|
28
|
+
fs.rmdirSync(path.dirname(imagePath));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore cleanup errors
|
|
32
|
+
}
|
|
33
|
+
vi.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
it('throws CommandExecutionError when image file does not exist', async () => {
|
|
36
|
+
await expect(imagexUpload('/nonexistent/path/cover.jpg', FAKE_UPLOAD_INFO)).rejects.toThrow(CommandExecutionError);
|
|
37
|
+
await expect(imagexUpload('/nonexistent/path/cover.jpg', FAKE_UPLOAD_INFO)).rejects.toThrow('Cover image file not found');
|
|
38
|
+
});
|
|
39
|
+
it('PUTs the image and returns store_uri on success', async () => {
|
|
40
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
41
|
+
ok: true,
|
|
42
|
+
status: 200,
|
|
43
|
+
text: vi.fn().mockResolvedValue(''),
|
|
44
|
+
});
|
|
45
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
46
|
+
const result = await imagexUpload(imagePath, FAKE_UPLOAD_INFO);
|
|
47
|
+
expect(result).toBe(FAKE_UPLOAD_INFO.store_uri);
|
|
48
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
49
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
50
|
+
expect(url).toBe(FAKE_UPLOAD_INFO.upload_url);
|
|
51
|
+
expect(init.method).toBe('PUT');
|
|
52
|
+
expect(init.headers['Content-Type']).toBe('image/jpeg');
|
|
53
|
+
});
|
|
54
|
+
it('uses image/png Content-Type for .png files', async () => {
|
|
55
|
+
const pngPath = makeTempImage('.png');
|
|
56
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
57
|
+
ok: true,
|
|
58
|
+
status: 200,
|
|
59
|
+
text: vi.fn().mockResolvedValue(''),
|
|
60
|
+
});
|
|
61
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
62
|
+
try {
|
|
63
|
+
await imagexUpload(pngPath, FAKE_UPLOAD_INFO);
|
|
64
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
65
|
+
expect(init.headers['Content-Type']).toBe('image/png');
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
try {
|
|
69
|
+
fs.unlinkSync(pngPath);
|
|
70
|
+
fs.rmdirSync(path.dirname(pngPath));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
it('throws CommandExecutionError on non-2xx PUT response', async () => {
|
|
78
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
79
|
+
ok: false,
|
|
80
|
+
status: 403,
|
|
81
|
+
text: vi.fn().mockResolvedValue('Forbidden'),
|
|
82
|
+
});
|
|
83
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
84
|
+
await expect(imagexUpload(imagePath, FAKE_UPLOAD_INFO)).rejects.toThrow(CommandExecutionError);
|
|
85
|
+
await expect(imagexUpload(imagePath, FAKE_UPLOAD_INFO)).rejects.toThrow('ImageX upload failed with status 403');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IPage } from '../../../types.js';
|
|
2
|
+
import type { Sts2Credentials } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fetch STS2 temporary credentials from the creator center.
|
|
5
|
+
* These are used to authenticate Node.js-side TOS multipart uploads.
|
|
6
|
+
* Returns: { access_key_id, secret_access_key, session_token, expired_time }
|
|
7
|
+
*/
|
|
8
|
+
export declare function getSts2Credentials(page: IPage): Promise<Sts2Credentials>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AuthRequiredError } from '../../../errors.js';
|
|
2
|
+
const STS2_URL = 'https://creator.douyin.com/aweme/mid/video/sts2/?scene=web&aid=1128&cookie_enabled=true&device_platform=web';
|
|
3
|
+
/**
|
|
4
|
+
* Fetch STS2 temporary credentials from the creator center.
|
|
5
|
+
* These are used to authenticate Node.js-side TOS multipart uploads.
|
|
6
|
+
* Returns: { access_key_id, secret_access_key, session_token, expired_time }
|
|
7
|
+
*/
|
|
8
|
+
export async function getSts2Credentials(page) {
|
|
9
|
+
const js = `fetch(${JSON.stringify(STS2_URL)}, { credentials: 'include' }).then(r => r.json())`;
|
|
10
|
+
const res = await page.evaluate(js);
|
|
11
|
+
if (!res?.data?.access_key_id) {
|
|
12
|
+
throw new AuthRequiredError('creator.douyin.com', 'STS2 credentials missing');
|
|
13
|
+
}
|
|
14
|
+
return res.data;
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface HashtagInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
id: number;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TextExtraItem {
|
|
8
|
+
type: number;
|
|
9
|
+
hashtag_id: number;
|
|
10
|
+
hashtag_name: string;
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
caption_start: number;
|
|
14
|
+
caption_end: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function parseTextExtra(_text: string, hashtags: HashtagInfo[]): TextExtraItem[];
|
|
17
|
+
/** Extract hashtag names from text (e.g. "#话题" → ["话题"]) */
|
|
18
|
+
export declare function extractHashtagNames(text: string): string[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function parseTextExtra(_text, hashtags) {
|
|
2
|
+
return hashtags.map((h) => ({
|
|
3
|
+
type: 1,
|
|
4
|
+
hashtag_id: h.id,
|
|
5
|
+
hashtag_name: h.name,
|
|
6
|
+
start: h.start,
|
|
7
|
+
end: h.end,
|
|
8
|
+
caption_start: 0,
|
|
9
|
+
caption_end: h.end - h.start,
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
/** Extract hashtag names from text (e.g. "#话题" → ["话题"]) */
|
|
13
|
+
export function extractHashtagNames(text) {
|
|
14
|
+
return [...text.matchAll(/#([^\s#]+)/g)].map((m) => m[1]);
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseTextExtra, extractHashtagNames } from './text-extra.js';
|
|
3
|
+
describe('parseTextExtra', () => {
|
|
4
|
+
it('returns empty array for text with no hashtags', () => {
|
|
5
|
+
const result = parseTextExtra('普通文本内容', []);
|
|
6
|
+
expect(result).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
it('produces type-1 entry for each hashtag', () => {
|
|
9
|
+
const hashtags = [
|
|
10
|
+
{ name: '话题', id: 12345, start: 5, end: 8 },
|
|
11
|
+
];
|
|
12
|
+
const result = parseTextExtra('普通文本 #话题', hashtags);
|
|
13
|
+
expect(result).toHaveLength(1);
|
|
14
|
+
expect(result[0]).toMatchObject({
|
|
15
|
+
type: 1,
|
|
16
|
+
hashtag_name: '话题',
|
|
17
|
+
hashtag_id: 12345,
|
|
18
|
+
start: 5,
|
|
19
|
+
end: 8,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('sets hashtag_id to 0 when not found', () => {
|
|
23
|
+
const hashtags = [
|
|
24
|
+
{ name: '未知话题', id: 0, start: 0, end: 5 },
|
|
25
|
+
];
|
|
26
|
+
const result = parseTextExtra('#未知话题', hashtags);
|
|
27
|
+
expect(result[0].hashtag_id).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('extractHashtagNames', () => {
|
|
31
|
+
it('extracts hashtag names from text', () => {
|
|
32
|
+
expect(extractHashtagNames('hello #foo and #bar')).toEqual(['foo', 'bar']);
|
|
33
|
+
});
|
|
34
|
+
it('returns empty array when no hashtags', () => {
|
|
35
|
+
expect(extractHashtagNames('no hashtags here')).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
});
|