@jackwener/opencli 1.3.3 → 1.4.1
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/pull_request_template.md +3 -1
- package/.github/workflows/build-extension.yml +7 -1
- package/.github/workflows/ci.yml +46 -6
- package/.github/workflows/docs.yml +1 -1
- package/.github/workflows/e2e-headed.yml +36 -3
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/security.yml +0 -3
- package/CHANGELOG.md +78 -0
- package/CONTRIBUTING.md +6 -3
- package/PRIVACY.md +57 -0
- package/README.md +31 -4
- package/README.zh-CN.md +31 -4
- package/SKILL.md +107 -2
- package/TESTING.md +1 -0
- package/chatwise-opencli.ps1 +82 -0
- package/dist/analysis.d.ts +38 -0
- package/dist/analysis.js +166 -0
- package/dist/browser/cdp.d.ts +0 -4
- package/dist/browser/cdp.js +53 -41
- package/dist/browser/cdp.test.d.ts +1 -0
- package/dist/browser/cdp.test.js +52 -0
- package/dist/browser/dom-snapshot.d.ts +2 -2
- package/dist/browser/dom-snapshot.js +54 -1
- package/dist/browser/dom-snapshot.test.js +36 -0
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/mcp.d.ts +0 -2
- package/dist/browser/mcp.js +2 -3
- package/dist/browser/page.d.ts +4 -3
- package/dist/browser/page.js +34 -37
- package/dist/browser/stealth.d.ts +0 -2
- package/dist/browser/stealth.js +24 -9
- package/dist/browser.test.js +2 -2
- package/dist/build-manifest.js +15 -9
- package/dist/build-manifest.test.js +12 -0
- package/dist/cascade.js +4 -2
- package/dist/cli-manifest.json +1325 -256
- package/dist/cli.js +57 -29
- package/dist/clis/_shared/desktop-commands.d.ts +22 -0
- package/dist/clis/_shared/desktop-commands.js +108 -0
- package/dist/clis/antigravity/serve.js +5 -2
- package/dist/clis/apple-podcasts/search.js +2 -1
- package/dist/clis/arxiv/search.js +3 -3
- package/dist/clis/bbc/news.js +0 -1
- package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
- package/dist/clis/bilibili/dynamic.test.js +68 -0
- package/dist/clis/bilibili/favorite.js +4 -2
- package/dist/clis/bilibili/following.js +3 -2
- package/dist/clis/bilibili/subtitle.js +8 -7
- package/dist/clis/bilibili/utils.js +2 -2
- package/dist/clis/boss/batchgreet.js +1 -1
- package/dist/clis/boss/chatlist.js +1 -1
- package/dist/clis/boss/chatmsg.js +1 -1
- package/dist/clis/boss/detail.js +1 -1
- package/dist/clis/boss/exchange.js +1 -1
- package/dist/clis/boss/greet.js +1 -1
- package/dist/clis/boss/invite.js +1 -1
- package/dist/clis/boss/joblist.js +1 -1
- package/dist/clis/boss/mark.js +4 -3
- package/dist/clis/boss/recommend.js +1 -1
- package/dist/clis/boss/resume.js +1 -1
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +5 -4
- package/dist/clis/boss/stats.js +1 -1
- package/dist/clis/chatgpt/ask.js +4 -0
- package/dist/clis/chatgpt/new.js +5 -1
- package/dist/clis/chatgpt/read.js +5 -1
- package/dist/clis/chatgpt/send.js +2 -1
- package/dist/clis/chatgpt/status.js +5 -1
- package/dist/clis/chatwise/ask.js +8 -2
- package/dist/clis/chatwise/export.js +2 -0
- package/dist/clis/chatwise/history.js +2 -0
- package/dist/clis/chatwise/model.js +8 -3
- package/dist/clis/chatwise/new.js +3 -18
- package/dist/clis/chatwise/read.js +2 -0
- package/dist/clis/chatwise/screenshot.js +3 -27
- package/dist/clis/chatwise/send.js +8 -2
- package/dist/clis/chatwise/shared.d.ts +2 -0
- package/dist/clis/chatwise/shared.js +6 -0
- package/dist/clis/chatwise/status.js +3 -22
- package/dist/clis/codex/ask.js +6 -2
- package/dist/clis/codex/dump.js +2 -25
- package/dist/clis/codex/new.js +2 -25
- package/dist/clis/codex/screenshot.js +2 -27
- package/dist/clis/codex/send.js +6 -4
- package/dist/clis/codex/status.js +2 -22
- package/dist/clis/ctrip/search.js +0 -1
- package/dist/clis/cursor/ask.js +2 -1
- package/dist/clis/cursor/composer.js +2 -1
- package/dist/clis/cursor/dump.js +2 -25
- package/dist/clis/cursor/new.js +2 -18
- package/dist/clis/cursor/read.js +2 -1
- package/dist/clis/cursor/screenshot.js +1 -30
- package/dist/clis/cursor/send.js +2 -1
- package/dist/clis/cursor/status.js +2 -21
- package/dist/clis/dictionary/examples.yaml +25 -0
- package/dist/clis/dictionary/search.yaml +27 -0
- package/dist/clis/dictionary/synonyms.yaml +25 -0
- package/dist/clis/douban/book-hot.js +1 -1
- package/dist/clis/douban/movie-hot.js +1 -1
- package/dist/clis/douban/search.js +1 -1
- package/dist/clis/douban/utils.d.ts +4 -1
- package/dist/clis/douban/utils.js +156 -1
- package/dist/clis/doubao/ask.js +1 -1
- package/dist/clis/doubao/new.js +1 -1
- package/dist/clis/doubao/read.js +1 -1
- package/dist/clis/doubao/send.js +1 -1
- package/dist/clis/doubao/status.js +1 -1
- package/dist/clis/doubao-app/ask.js +1 -1
- package/dist/clis/doubao-app/new.js +1 -1
- package/dist/clis/doubao-app/read.js +1 -1
- package/dist/clis/doubao-app/send.js +1 -1
- package/dist/clis/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/grok/ask.d.ts +4 -0
- package/dist/clis/grok/ask.js +28 -10
- package/dist/clis/grok/ask.test.js +18 -0
- package/dist/clis/hackernews/search.yaml +1 -1
- package/dist/clis/instagram/search.yaml +2 -1
- package/dist/clis/jd/item.d.ts +1 -0
- package/dist/clis/jd/item.js +96 -0
- package/dist/clis/jd/item.test.d.ts +1 -0
- package/dist/clis/jd/item.test.js +28 -0
- package/dist/clis/jike/feed.js +1 -1
- package/dist/clis/jike/search.js +1 -1
- package/dist/clis/linkedin/search.js +5 -4
- package/dist/clis/linkedin/timeline.d.ts +21 -0
- package/dist/clis/linkedin/timeline.js +503 -0
- package/dist/clis/linkedin/timeline.test.d.ts +1 -0
- package/dist/clis/linkedin/timeline.test.js +81 -0
- package/dist/clis/linux-do/search.yaml +3 -1
- package/dist/clis/medium/feed.js +1 -1
- package/dist/clis/medium/search.js +2 -2
- package/dist/clis/medium/user.js +1 -1
- package/dist/clis/medium/{shared.js → utils.js} +2 -1
- package/dist/clis/pixiv/detail.yaml +49 -0
- package/dist/clis/pixiv/download.d.ts +7 -0
- package/dist/clis/pixiv/download.js +78 -0
- package/dist/clis/pixiv/download.test.d.ts +1 -0
- package/dist/clis/pixiv/download.test.js +87 -0
- package/dist/clis/pixiv/illusts.d.ts +8 -0
- package/dist/clis/pixiv/illusts.js +65 -0
- package/dist/clis/pixiv/illusts.test.d.ts +1 -0
- package/dist/clis/pixiv/illusts.test.js +99 -0
- package/dist/clis/pixiv/ranking.yaml +53 -0
- package/dist/clis/pixiv/search.d.ts +6 -0
- package/dist/clis/pixiv/search.js +43 -0
- package/dist/clis/pixiv/search.test.d.ts +1 -0
- package/dist/clis/pixiv/search.test.js +83 -0
- package/dist/clis/pixiv/test-utils.d.ts +12 -0
- package/dist/clis/pixiv/test-utils.js +23 -0
- package/dist/clis/pixiv/user.yaml +46 -0
- package/dist/clis/pixiv/utils.d.ts +27 -0
- package/dist/clis/pixiv/utils.js +49 -0
- package/dist/clis/reddit/comment.js +2 -1
- package/dist/clis/reddit/read.js +4 -3
- package/dist/clis/reddit/read.test.d.ts +1 -0
- package/dist/clis/reddit/read.test.js +28 -0
- package/dist/clis/reddit/save.js +2 -1
- package/dist/clis/reddit/saved.js +7 -3
- package/dist/clis/reddit/subscribe.js +2 -1
- package/dist/clis/reddit/upvote.js +2 -1
- package/dist/clis/reddit/upvoted.js +7 -3
- package/dist/clis/reuters/search.js +0 -1
- package/dist/clis/sinablog/article.js +1 -1
- package/dist/clis/sinablog/hot.js +1 -1
- package/dist/clis/sinablog/user.js +1 -1
- package/dist/clis/substack/feed.js +1 -1
- package/dist/clis/substack/publication.js +1 -1
- package/dist/clis/substack/search.js +3 -2
- package/dist/clis/substack/{shared.js → utils.js} +3 -2
- package/dist/clis/tiktok/search.yaml +2 -1
- package/dist/clis/twitter/accept.js +2 -1
- package/dist/clis/twitter/article.js +4 -1
- package/dist/clis/twitter/block.js +2 -1
- package/dist/clis/twitter/bookmark.js +2 -1
- package/dist/clis/twitter/bookmarks.js +3 -2
- package/dist/clis/twitter/delete.js +2 -1
- package/dist/clis/twitter/follow.js +2 -1
- package/dist/clis/twitter/followers.js +3 -2
- package/dist/clis/twitter/following.js +3 -2
- package/dist/clis/twitter/hide-reply.js +2 -1
- package/dist/clis/twitter/like.js +2 -1
- package/dist/clis/twitter/notifications.js +2 -1
- package/dist/clis/twitter/post.js +2 -1
- package/dist/clis/twitter/profile.js +5 -2
- package/dist/clis/twitter/reply-dm.js +2 -1
- package/dist/clis/twitter/reply.js +2 -1
- package/dist/clis/twitter/search.js +32 -13
- package/dist/clis/twitter/search.test.d.ts +1 -0
- package/dist/clis/twitter/search.test.js +156 -0
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/timeline.js +3 -2
- package/dist/clis/twitter/trending.js +3 -2
- package/dist/clis/twitter/unblock.js +2 -1
- package/dist/clis/twitter/unbookmark.js +2 -1
- package/dist/clis/twitter/unfollow.js +2 -1
- package/dist/clis/v2ex/daily.js +3 -2
- package/dist/clis/v2ex/me.js +3 -2
- package/dist/clis/v2ex/notifications.js +4 -4
- package/dist/clis/web/read.d.ts +16 -0
- package/dist/clis/web/read.js +202 -0
- package/dist/clis/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/weread/search.js +3 -2
- package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
- package/dist/clis/xueqiu/danjuan-utils.js +126 -0
- package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
- package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
- package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
- package/dist/clis/xueqiu/fund-holdings.js +28 -0
- package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
- package/dist/clis/xueqiu/fund-snapshot.js +25 -0
- package/dist/clis/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/youtube/transcript.js +5 -4
- package/dist/clis/youtube/video.js +3 -2
- package/dist/clis/zhihu/search.yaml +2 -1
- package/dist/daemon.js +7 -3
- package/dist/discovery.js +11 -10
- package/dist/doctor.js +2 -1
- package/dist/download/index.d.ts +4 -12
- package/dist/download/index.js +33 -12
- package/dist/download/index.test.js +79 -2
- package/dist/download/media-download.js +4 -2
- package/dist/engine.test.js +76 -4
- package/dist/execution.d.ts +1 -9
- package/dist/execution.js +56 -46
- package/dist/explore.js +12 -111
- package/dist/external-clis.yaml +0 -25
- package/dist/external.js +7 -5
- package/dist/external.test.js +4 -0
- package/dist/generate.d.ts +0 -9
- package/dist/generate.js +4 -20
- package/dist/hooks.d.ts +46 -0
- package/dist/hooks.js +56 -0
- package/dist/hooks.test.d.ts +4 -0
- package/dist/hooks.test.js +92 -0
- package/dist/interceptor.js +70 -23
- package/dist/main.js +2 -0
- package/dist/output.js +12 -6
- package/dist/pipeline/executor.js +1 -1
- package/dist/pipeline/steps/browser.js +1 -3
- package/dist/pipeline/steps/download.js +42 -26
- package/dist/pipeline/steps/download.test.d.ts +1 -0
- package/dist/pipeline/steps/download.test.js +101 -0
- package/dist/pipeline/steps/fetch.js +40 -22
- package/dist/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/pipeline/steps/fetch.test.js +123 -0
- package/dist/pipeline/steps/transform.js +2 -6
- package/dist/pipeline/template.js +66 -52
- package/dist/pipeline/template.test.js +28 -0
- package/dist/pipeline/transform.test.js +18 -0
- package/dist/plugin.d.ts +40 -1
- package/dist/plugin.js +214 -17
- package/dist/plugin.test.d.ts +1 -1
- package/dist/plugin.test.js +219 -3
- package/dist/record.js +6 -98
- package/dist/registry-api.d.ts +2 -0
- package/dist/registry-api.js +1 -0
- package/dist/registry.d.ts +5 -2
- package/dist/registry.js +1 -2
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +14 -4
- package/dist/snapshotFormatter.d.ts +7 -14
- package/dist/snapshotFormatter.js +38 -78
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +29 -0
- package/dist/validate.js +3 -5
- package/dist/weread-search-regression.test.d.ts +1 -0
- package/dist/weread-search-regression.test.js +39 -0
- package/dist/yaml-schema.d.ts +26 -0
- package/dist/yaml-schema.js +5 -0
- package/docs/.vitepress/config.mts +16 -0
- package/docs/adapters/browser/dictionary.md +27 -0
- package/docs/adapters/browser/douyin.md +75 -0
- package/docs/adapters/browser/jd.md +27 -0
- package/docs/adapters/browser/linkedin.md +6 -0
- package/docs/adapters/browser/pixiv.md +92 -0
- package/docs/adapters/browser/twitter.md +6 -0
- package/docs/adapters/browser/web.md +30 -0
- package/docs/adapters/browser/xueqiu.md +27 -9
- package/docs/adapters/index.md +9 -2
- package/docs/comparison.md +125 -0
- package/docs/developer/contributing.md +21 -2
- package/docs/developer/testing.md +14 -8
- package/docs/developer/ts-adapter.md +18 -0
- package/docs/developer/yaml-adapter.md +16 -0
- package/docs/guide/plugins.md +10 -0
- package/docs/zh/guide/plugins.md +10 -0
- package/extension/dist/background.js +100 -35
- package/extension/manifest.json +6 -2
- package/extension/package.json +1 -1
- package/extension/popup.html +84 -0
- package/extension/popup.js +25 -0
- package/extension/src/background.test.ts +46 -1
- package/extension/src/background.ts +128 -34
- package/extension/src/cdp.ts +9 -9
- package/package.json +3 -2
- package/scripts/check-doc-coverage.sh +2 -0
- package/src/analysis.ts +170 -0
- package/src/browser/cdp.test.ts +66 -0
- package/src/browser/cdp.ts +59 -44
- package/src/browser/dom-snapshot.test.ts +42 -0
- package/src/browser/dom-snapshot.ts +56 -3
- package/src/browser/index.ts +2 -2
- package/src/browser/mcp.ts +2 -4
- package/src/browser/page.ts +34 -37
- package/src/browser/stealth.ts +24 -10
- package/src/browser.test.ts +2 -2
- package/src/build-manifest.test.ts +14 -0
- package/src/build-manifest.ts +13 -31
- package/src/cascade.ts +5 -3
- package/src/cli.ts +66 -34
- package/src/clis/_shared/desktop-commands.ts +121 -0
- package/src/clis/antigravity/serve.ts +6 -3
- package/src/clis/apple-podcasts/search.ts +2 -1
- package/src/clis/arxiv/search.ts +3 -3
- package/src/clis/bbc/news.ts +0 -1
- package/src/clis/bilibili/dynamic.test.ts +79 -0
- package/src/clis/bilibili/favorite.ts +5 -2
- package/src/clis/bilibili/following.ts +3 -2
- package/src/clis/bilibili/subtitle.ts +8 -7
- package/src/clis/bilibili/utils.ts +2 -2
- package/src/clis/boss/batchgreet.ts +1 -1
- package/src/clis/boss/chatlist.ts +1 -1
- package/src/clis/boss/chatmsg.ts +1 -1
- package/src/clis/boss/detail.ts +1 -1
- package/src/clis/boss/exchange.ts +1 -1
- package/src/clis/boss/greet.ts +1 -1
- package/src/clis/boss/invite.ts +1 -1
- package/src/clis/boss/joblist.ts +1 -1
- package/src/clis/boss/mark.ts +4 -3
- package/src/clis/boss/recommend.ts +1 -1
- package/src/clis/boss/resume.ts +1 -1
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +5 -4
- package/src/clis/boss/stats.ts +1 -1
- package/src/clis/chatgpt/ask.ts +5 -0
- package/src/clis/chatgpt/new.ts +7 -2
- package/src/clis/chatgpt/read.ts +7 -2
- package/src/clis/chatgpt/send.ts +3 -2
- package/src/clis/chatgpt/status.ts +6 -1
- package/src/clis/chatwise/ask.ts +7 -2
- package/src/clis/chatwise/export.ts +2 -0
- package/src/clis/chatwise/history.ts +2 -0
- package/src/clis/chatwise/model.ts +7 -3
- package/src/clis/chatwise/new.ts +3 -20
- package/src/clis/chatwise/read.ts +2 -0
- package/src/clis/chatwise/screenshot.ts +3 -32
- package/src/clis/chatwise/send.ts +7 -2
- package/src/clis/chatwise/shared.ts +8 -0
- package/src/clis/chatwise/status.ts +3 -24
- package/src/clis/codex/ask.ts +5 -2
- package/src/clis/codex/dump.ts +2 -27
- package/src/clis/codex/new.ts +2 -28
- package/src/clis/codex/screenshot.ts +2 -32
- package/src/clis/codex/send.ts +5 -4
- package/src/clis/codex/status.ts +2 -24
- package/src/clis/ctrip/search.ts +0 -1
- package/src/clis/cursor/ask.ts +2 -1
- package/src/clis/cursor/composer.ts +2 -1
- package/src/clis/cursor/dump.ts +2 -27
- package/src/clis/cursor/new.ts +2 -20
- package/src/clis/cursor/read.ts +2 -1
- package/src/clis/cursor/screenshot.ts +1 -36
- package/src/clis/cursor/send.ts +2 -1
- package/src/clis/cursor/status.ts +2 -22
- package/src/clis/dictionary/examples.yaml +25 -0
- package/src/clis/dictionary/search.yaml +27 -0
- package/src/clis/dictionary/synonyms.yaml +25 -0
- package/src/clis/douban/book-hot.ts +1 -1
- package/src/clis/douban/movie-hot.ts +1 -1
- package/src/clis/douban/search.ts +1 -1
- package/src/clis/douban/utils.ts +165 -1
- package/src/clis/doubao/ask.ts +1 -1
- package/src/clis/doubao/new.ts +1 -1
- package/src/clis/doubao/read.ts +1 -1
- package/src/clis/doubao/send.ts +1 -1
- package/src/clis/doubao/status.ts +1 -1
- package/src/clis/doubao-app/ask.ts +1 -1
- package/src/clis/doubao-app/new.ts +1 -1
- package/src/clis/doubao-app/read.ts +1 -1
- package/src/clis/doubao-app/send.ts +1 -1
- package/src/clis/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/grok/ask.test.ts +25 -0
- package/src/clis/grok/ask.ts +25 -12
- package/src/clis/hackernews/search.yaml +1 -1
- package/src/clis/instagram/search.yaml +2 -1
- package/src/clis/jd/item.test.ts +35 -0
- package/src/clis/jd/item.ts +101 -0
- package/src/clis/jike/feed.ts +1 -1
- package/src/clis/jike/search.ts +1 -1
- package/src/clis/linkedin/search.ts +5 -4
- package/src/clis/linkedin/timeline.test.ts +99 -0
- package/src/clis/linkedin/timeline.ts +532 -0
- package/src/clis/linux-do/search.yaml +3 -1
- package/src/clis/medium/feed.ts +1 -1
- package/src/clis/medium/search.ts +2 -2
- package/src/clis/medium/user.ts +1 -1
- package/src/clis/medium/{shared.ts → utils.ts} +2 -1
- package/src/clis/pixiv/detail.yaml +49 -0
- package/src/clis/pixiv/download.test.ts +114 -0
- package/src/clis/pixiv/download.ts +91 -0
- package/src/clis/pixiv/illusts.test.ts +115 -0
- package/src/clis/pixiv/illusts.ts +78 -0
- package/src/clis/pixiv/ranking.yaml +53 -0
- package/src/clis/pixiv/search.test.ts +97 -0
- package/src/clis/pixiv/search.ts +53 -0
- package/src/clis/pixiv/test-utils.ts +29 -0
- package/src/clis/pixiv/user.yaml +46 -0
- package/src/clis/pixiv/utils.ts +62 -0
- package/src/clis/reddit/comment.ts +2 -1
- package/src/clis/reddit/read.test.ts +34 -0
- package/src/clis/reddit/read.ts +4 -3
- package/src/clis/reddit/save.ts +2 -1
- package/src/clis/reddit/saved.ts +6 -2
- package/src/clis/reddit/subscribe.ts +2 -1
- package/src/clis/reddit/upvote.ts +2 -1
- package/src/clis/reddit/upvoted.ts +6 -2
- package/src/clis/reuters/search.ts +0 -1
- package/src/clis/sinablog/article.ts +1 -1
- package/src/clis/sinablog/hot.ts +1 -1
- package/src/clis/sinablog/user.ts +1 -1
- package/src/clis/substack/feed.ts +1 -1
- package/src/clis/substack/publication.ts +1 -1
- package/src/clis/substack/search.ts +3 -2
- package/src/clis/substack/{shared.ts → utils.ts} +3 -2
- package/src/clis/tiktok/search.yaml +2 -1
- package/src/clis/twitter/accept.ts +2 -1
- package/src/clis/twitter/article.ts +3 -1
- package/src/clis/twitter/block.ts +2 -1
- package/src/clis/twitter/bookmark.ts +2 -1
- package/src/clis/twitter/bookmarks.ts +3 -2
- package/src/clis/twitter/delete.ts +2 -1
- package/src/clis/twitter/follow.ts +2 -1
- package/src/clis/twitter/followers.ts +3 -2
- package/src/clis/twitter/following.ts +3 -2
- package/src/clis/twitter/hide-reply.ts +2 -1
- package/src/clis/twitter/like.ts +2 -1
- package/src/clis/twitter/notifications.ts +2 -1
- package/src/clis/twitter/post.ts +2 -1
- package/src/clis/twitter/profile.ts +4 -2
- package/src/clis/twitter/reply-dm.ts +2 -1
- package/src/clis/twitter/reply.ts +2 -1
- package/src/clis/twitter/search.test.ts +180 -0
- package/src/clis/twitter/search.ts +40 -14
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/timeline.ts +3 -2
- package/src/clis/twitter/trending.ts +3 -2
- package/src/clis/twitter/unblock.ts +2 -1
- package/src/clis/twitter/unbookmark.ts +2 -1
- package/src/clis/twitter/unfollow.ts +2 -1
- package/src/clis/v2ex/daily.ts +3 -2
- package/src/clis/v2ex/me.ts +3 -2
- package/src/clis/v2ex/notifications.ts +3 -4
- package/src/clis/web/read.ts +210 -0
- package/src/clis/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/weread/search.ts +3 -2
- package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
- package/src/clis/xueqiu/danjuan-utils.ts +176 -0
- package/src/clis/xueqiu/fund-holdings.ts +32 -0
- package/src/clis/xueqiu/fund-snapshot.ts +27 -0
- package/src/clis/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/youtube/transcript.ts +5 -4
- package/src/clis/youtube/video.ts +3 -2
- package/src/clis/zhihu/search.yaml +2 -1
- package/src/daemon.ts +5 -4
- package/src/discovery.ts +12 -34
- package/src/doctor.ts +3 -2
- package/src/download/index.test.ts +93 -2
- package/src/download/index.ts +44 -23
- package/src/download/media-download.ts +5 -3
- package/src/engine.test.ts +84 -3
- package/src/execution.ts +62 -46
- package/src/explore.ts +21 -90
- package/src/external-clis.yaml +0 -25
- package/src/external.test.ts +9 -0
- package/src/external.ts +12 -10
- package/src/generate.ts +4 -41
- package/src/hooks.test.ts +126 -0
- package/src/hooks.ts +90 -0
- package/src/interceptor.ts +73 -23
- package/src/main.ts +2 -0
- package/src/output.ts +14 -6
- package/src/pipeline/executor.ts +1 -1
- package/src/pipeline/steps/browser.ts +1 -3
- package/src/pipeline/steps/download.test.ts +136 -0
- package/src/pipeline/steps/download.ts +47 -34
- package/src/pipeline/steps/fetch.test.ts +179 -0
- package/src/pipeline/steps/fetch.ts +39 -23
- package/src/pipeline/steps/transform.ts +2 -6
- package/src/pipeline/template.test.ts +28 -0
- package/src/pipeline/template.ts +67 -79
- package/src/pipeline/transform.test.ts +20 -0
- package/src/plugin.test.ts +251 -3
- package/src/plugin.ts +265 -21
- package/src/record.ts +12 -84
- package/src/registry-api.ts +2 -0
- package/src/registry.ts +7 -4
- package/src/runtime.ts +14 -4
- package/src/snapshotFormatter.ts +43 -121
- package/src/utils.ts +39 -0
- package/src/validate.ts +3 -5
- package/src/weread-search-regression.test.ts +44 -0
- package/src/yaml-schema.ts +28 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
- package/tests/e2e/browser-public-extended.test.ts +162 -0
- package/tests/e2e/browser-public.test.ts +7 -146
- package/tests/e2e/plugin-management.test.ts +137 -0
- package/tests/e2e/public-commands.test.ts +34 -1
- package/vitest.config.ts +33 -8
- package/.github/workflows/pkg-pr-new.yml +0 -30
- package/dist/clis/douban/shared.d.ts +0 -4
- package/dist/clis/douban/shared.js +0 -155
- package/src/clis/douban/shared.ts +0 -165
- /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/boss/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
- /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/jike/{shared.js → utils.js} +0 -0
- /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
- /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
- /package/src/clis/boss/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
- /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
- /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOS (ByteDance Object Storage) multipart uploader with resume support.
|
|
3
|
+
*
|
|
4
|
+
* Uses AWS Signature V4 (HMAC-SHA256) with STS2 temporary credentials.
|
|
5
|
+
* For the init multipart upload call, the pre-computed auth from TosUploadInfo is used.
|
|
6
|
+
* For PUT part uploads and the final complete call, AWS4 is computed from STS2 credentials.
|
|
7
|
+
*/
|
|
8
|
+
import * as crypto from 'node:crypto';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { CommandExecutionError } from '../../../errors.js';
|
|
13
|
+
const PART_SIZE = 5 * 1024 * 1024; // 5 MB minimum per TOS/S3 spec
|
|
14
|
+
const RESUME_DIR = path.join(os.homedir(), '.opencli', 'douyin-resume');
|
|
15
|
+
// ── Resume file helpers ──────────────────────────────────────────────────────
|
|
16
|
+
function getResumeFilePath(filePath) {
|
|
17
|
+
const hash = crypto.createHash('sha256').update(filePath).digest('hex');
|
|
18
|
+
return path.join(RESUME_DIR, `${hash}.json`);
|
|
19
|
+
}
|
|
20
|
+
function loadResumeState(resumePath, fileSize) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(resumePath, 'utf8');
|
|
23
|
+
const state = JSON.parse(raw);
|
|
24
|
+
if (state.fileSize === fileSize && state.uploadId && Array.isArray(state.parts)) {
|
|
25
|
+
return state;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// no valid resume state
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function saveResumeState(resumePath, state) {
|
|
34
|
+
fs.mkdirSync(path.dirname(resumePath), { recursive: true });
|
|
35
|
+
fs.writeFileSync(resumePath, JSON.stringify(state, null, 2), 'utf8');
|
|
36
|
+
}
|
|
37
|
+
function deleteResumeState(resumePath) {
|
|
38
|
+
try {
|
|
39
|
+
fs.unlinkSync(resumePath);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore if not found
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ── AWS Signature V4 ─────────────────────────────────────────────────────────
|
|
46
|
+
function hmacSha256(key, data) {
|
|
47
|
+
return crypto.createHmac('sha256', key).update(data, 'utf8').digest();
|
|
48
|
+
}
|
|
49
|
+
function sha256Hex(data) {
|
|
50
|
+
const hash = crypto.createHash('sha256');
|
|
51
|
+
if (typeof data === 'string') {
|
|
52
|
+
hash.update(data, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
hash.update(data);
|
|
56
|
+
}
|
|
57
|
+
return hash.digest('hex');
|
|
58
|
+
}
|
|
59
|
+
function extractRegionFromHost(host) {
|
|
60
|
+
// e.g. "tos-cn-i-alisg.volces.com" → "cn-i-alisg"
|
|
61
|
+
// e.g. "tos-cn-beijing.ivolces.com" → "cn-beijing"
|
|
62
|
+
const match = host.match(/^tos-([^.]+)\./);
|
|
63
|
+
if (match)
|
|
64
|
+
return match[1];
|
|
65
|
+
return 'cn-north-1'; // fallback
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compute AWS Signature V4 headers for a TOS request.
|
|
69
|
+
* Returns a Record of all headers to include (including Authorization, x-amz-date, etc.)
|
|
70
|
+
*/
|
|
71
|
+
function computeAws4Headers(opts) {
|
|
72
|
+
const { method, url, credentials, service, region, datetime } = opts;
|
|
73
|
+
const date = datetime.slice(0, 8); // YYYYMMDD
|
|
74
|
+
const parsedUrl = new URL(url);
|
|
75
|
+
const canonicalUri = parsedUrl.pathname || '/';
|
|
76
|
+
// Canonical query string: sort by name, encode
|
|
77
|
+
const queryParams = [...parsedUrl.searchParams.entries()]
|
|
78
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
79
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
80
|
+
.join('&');
|
|
81
|
+
const bodyHash = sha256Hex(opts.body);
|
|
82
|
+
// Merge in required headers and compute canonical headers
|
|
83
|
+
const allHeaders = {
|
|
84
|
+
...opts.headers,
|
|
85
|
+
host: parsedUrl.host,
|
|
86
|
+
'x-amz-content-sha256': bodyHash,
|
|
87
|
+
'x-amz-date': datetime,
|
|
88
|
+
'x-amz-security-token': credentials.session_token,
|
|
89
|
+
};
|
|
90
|
+
const sortedHeaderKeys = Object.keys(allHeaders).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
91
|
+
const canonicalHeaders = sortedHeaderKeys
|
|
92
|
+
.map(k => `${k.toLowerCase()}:${allHeaders[k].trim()}`)
|
|
93
|
+
.join('\n') + '\n';
|
|
94
|
+
const signedHeadersList = sortedHeaderKeys.map(k => k.toLowerCase()).join(';');
|
|
95
|
+
const canonicalRequest = [
|
|
96
|
+
method.toUpperCase(),
|
|
97
|
+
canonicalUri,
|
|
98
|
+
queryParams,
|
|
99
|
+
canonicalHeaders,
|
|
100
|
+
signedHeadersList,
|
|
101
|
+
bodyHash,
|
|
102
|
+
].join('\n');
|
|
103
|
+
const credentialScope = `${date}/${region}/${service}/aws4_request`;
|
|
104
|
+
const stringToSign = [
|
|
105
|
+
'AWS4-HMAC-SHA256',
|
|
106
|
+
datetime,
|
|
107
|
+
credentialScope,
|
|
108
|
+
sha256Hex(canonicalRequest),
|
|
109
|
+
].join('\n');
|
|
110
|
+
// Signing key chain
|
|
111
|
+
const kDate = hmacSha256(`AWS4${credentials.secret_access_key}`, date);
|
|
112
|
+
const kRegion = hmacSha256(kDate, region);
|
|
113
|
+
const kService = hmacSha256(kRegion, service);
|
|
114
|
+
const kSigning = hmacSha256(kService, 'aws4_request');
|
|
115
|
+
const signature = hmacSha256(kSigning, stringToSign).toString('hex');
|
|
116
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${credentials.access_key_id}/${credentialScope}, SignedHeaders=${signedHeadersList}, Signature=${signature}`;
|
|
117
|
+
return {
|
|
118
|
+
...allHeaders,
|
|
119
|
+
Authorization: authorization,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
123
|
+
async function tosRequest(opts) {
|
|
124
|
+
const { method, url, headers, body } = opts;
|
|
125
|
+
const fetchBody = body == null ? null
|
|
126
|
+
: typeof body === 'string' ? body
|
|
127
|
+
: body;
|
|
128
|
+
const res = await fetch(url, {
|
|
129
|
+
method,
|
|
130
|
+
headers,
|
|
131
|
+
body: fetchBody,
|
|
132
|
+
});
|
|
133
|
+
const responseBody = await res.text();
|
|
134
|
+
const responseHeaders = {};
|
|
135
|
+
res.headers.forEach((value, key) => {
|
|
136
|
+
responseHeaders[key.toLowerCase()] = value;
|
|
137
|
+
});
|
|
138
|
+
return { status: res.status, headers: responseHeaders, body: responseBody };
|
|
139
|
+
}
|
|
140
|
+
function nowDatetime() {
|
|
141
|
+
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
|
|
142
|
+
}
|
|
143
|
+
// ── Phase 1: Init multipart upload ───────────────────────────────────────────
|
|
144
|
+
async function initMultipartUpload(tosUrl, auth, credentials) {
|
|
145
|
+
const initUrl = `${tosUrl}?uploads`;
|
|
146
|
+
const datetime = nowDatetime();
|
|
147
|
+
// Use the pre-computed auth for INIT, as it comes from ApplyVideoUpload
|
|
148
|
+
const headers = {
|
|
149
|
+
Authorization: auth,
|
|
150
|
+
'x-amz-date': datetime,
|
|
151
|
+
'x-amz-security-token': credentials.session_token,
|
|
152
|
+
'content-type': 'application/octet-stream',
|
|
153
|
+
};
|
|
154
|
+
const res = await tosRequest({ method: 'POST', url: initUrl, headers });
|
|
155
|
+
if (res.status !== 200) {
|
|
156
|
+
throw new CommandExecutionError(`TOS init multipart upload failed with status ${res.status}: ${res.body}`, 'Check that TOS credentials are valid and not expired.');
|
|
157
|
+
}
|
|
158
|
+
// Parse UploadId from XML: <UploadId>...</UploadId>
|
|
159
|
+
const match = res.body.match(/<UploadId>([^<]+)<\/UploadId>/);
|
|
160
|
+
if (!match) {
|
|
161
|
+
throw new CommandExecutionError(`TOS init response missing UploadId: ${res.body}`);
|
|
162
|
+
}
|
|
163
|
+
return match[1];
|
|
164
|
+
}
|
|
165
|
+
// ── Phase 2: Upload a single part ────────────────────────────────────────────
|
|
166
|
+
async function uploadPart(tosUrl, partNumber, uploadId, data, credentials, region) {
|
|
167
|
+
const parsedUrl = new URL(tosUrl);
|
|
168
|
+
parsedUrl.searchParams.set('partNumber', String(partNumber));
|
|
169
|
+
parsedUrl.searchParams.set('uploadId', uploadId);
|
|
170
|
+
const url = parsedUrl.toString();
|
|
171
|
+
const datetime = nowDatetime();
|
|
172
|
+
const headers = computeAws4Headers({
|
|
173
|
+
method: 'PUT',
|
|
174
|
+
url,
|
|
175
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
176
|
+
body: data,
|
|
177
|
+
credentials,
|
|
178
|
+
service: 'tos',
|
|
179
|
+
region,
|
|
180
|
+
datetime,
|
|
181
|
+
});
|
|
182
|
+
const res = await tosRequest({ method: 'PUT', url, headers, body: data });
|
|
183
|
+
if (res.status !== 200) {
|
|
184
|
+
throw new CommandExecutionError(`TOS upload part ${partNumber} failed with status ${res.status}: ${res.body}`, 'Check that STS2 credentials are valid and not expired.');
|
|
185
|
+
}
|
|
186
|
+
const etag = res.headers['etag'];
|
|
187
|
+
if (!etag) {
|
|
188
|
+
throw new CommandExecutionError(`TOS upload part ${partNumber} response missing ETag header`);
|
|
189
|
+
}
|
|
190
|
+
return etag;
|
|
191
|
+
}
|
|
192
|
+
// ── Phase 3: Complete multipart upload ───────────────────────────────────────
|
|
193
|
+
async function completeMultipartUpload(tosUrl, uploadId, parts, credentials, region) {
|
|
194
|
+
const parsedUrl = new URL(tosUrl);
|
|
195
|
+
parsedUrl.searchParams.set('uploadId', uploadId);
|
|
196
|
+
const url = parsedUrl.toString();
|
|
197
|
+
const xmlBody = '<CompleteMultipartUpload>' +
|
|
198
|
+
parts
|
|
199
|
+
.sort((a, b) => a.partNumber - b.partNumber)
|
|
200
|
+
.map(p => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>${p.etag}</ETag></Part>`)
|
|
201
|
+
.join('') +
|
|
202
|
+
'</CompleteMultipartUpload>';
|
|
203
|
+
const datetime = nowDatetime();
|
|
204
|
+
const headers = computeAws4Headers({
|
|
205
|
+
method: 'POST',
|
|
206
|
+
url,
|
|
207
|
+
headers: { 'content-type': 'application/xml' },
|
|
208
|
+
body: xmlBody,
|
|
209
|
+
credentials,
|
|
210
|
+
service: 'tos',
|
|
211
|
+
region,
|
|
212
|
+
datetime,
|
|
213
|
+
});
|
|
214
|
+
const res = await tosRequest({
|
|
215
|
+
method: 'POST',
|
|
216
|
+
url,
|
|
217
|
+
headers,
|
|
218
|
+
body: xmlBody,
|
|
219
|
+
});
|
|
220
|
+
if (res.status !== 200) {
|
|
221
|
+
throw new CommandExecutionError(`TOS complete multipart upload failed with status ${res.status}: ${res.body}`, 'Check that all parts were uploaded successfully.');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
let _readSyncOverride = null;
|
|
225
|
+
/** @internal — for testing only */
|
|
226
|
+
export function setReadSyncOverride(fn) {
|
|
227
|
+
_readSyncOverride = fn;
|
|
228
|
+
}
|
|
229
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
230
|
+
export async function tosUpload(options) {
|
|
231
|
+
const { filePath, uploadInfo, credentials, onProgress } = options;
|
|
232
|
+
// Validate file exists
|
|
233
|
+
if (!fs.existsSync(filePath)) {
|
|
234
|
+
throw new CommandExecutionError(`Video file not found: ${filePath}`, 'Ensure the file path is correct and accessible.');
|
|
235
|
+
}
|
|
236
|
+
const { size: fileSize } = fs.statSync(filePath);
|
|
237
|
+
if (fileSize === 0) {
|
|
238
|
+
throw new CommandExecutionError(`Video file is empty: ${filePath}`);
|
|
239
|
+
}
|
|
240
|
+
const { tos_upload_url: tosUrl, auth } = uploadInfo;
|
|
241
|
+
const parsedTosUrl = new URL(tosUrl);
|
|
242
|
+
const region = extractRegionFromHost(parsedTosUrl.host);
|
|
243
|
+
const resumePath = getResumeFilePath(filePath);
|
|
244
|
+
let resumeState = loadResumeState(resumePath, fileSize);
|
|
245
|
+
let uploadId;
|
|
246
|
+
let completedParts;
|
|
247
|
+
if (resumeState) {
|
|
248
|
+
// Resume from previous state
|
|
249
|
+
uploadId = resumeState.uploadId;
|
|
250
|
+
completedParts = resumeState.parts;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Start fresh
|
|
254
|
+
uploadId = await initMultipartUpload(tosUrl, auth, credentials);
|
|
255
|
+
completedParts = [];
|
|
256
|
+
saveResumeState(resumePath, { uploadId, fileSize, parts: completedParts });
|
|
257
|
+
}
|
|
258
|
+
// Determine which parts are already done
|
|
259
|
+
const completedPartNumbers = new Set(completedParts.map(p => p.partNumber));
|
|
260
|
+
// Calculate total parts
|
|
261
|
+
const totalParts = Math.ceil(fileSize / PART_SIZE);
|
|
262
|
+
// Track uploaded bytes for progress
|
|
263
|
+
let uploadedBytes = completedParts.length * PART_SIZE;
|
|
264
|
+
if (onProgress)
|
|
265
|
+
onProgress(Math.min(uploadedBytes, fileSize), fileSize);
|
|
266
|
+
const fd = fs.openSync(filePath, 'r');
|
|
267
|
+
try {
|
|
268
|
+
for (let partNumber = 1; partNumber <= totalParts; partNumber++) {
|
|
269
|
+
if (completedPartNumbers.has(partNumber)) {
|
|
270
|
+
continue; // already uploaded
|
|
271
|
+
}
|
|
272
|
+
const offset = (partNumber - 1) * PART_SIZE;
|
|
273
|
+
const chunkSize = Math.min(PART_SIZE, fileSize - offset);
|
|
274
|
+
const buffer = Buffer.allocUnsafe(chunkSize);
|
|
275
|
+
const readFn = _readSyncOverride ?? fs.readSync;
|
|
276
|
+
const bytesRead = readFn(fd, buffer, 0, chunkSize, offset);
|
|
277
|
+
if (bytesRead !== chunkSize) {
|
|
278
|
+
throw new CommandExecutionError(`Short read on part ${partNumber}: expected ${chunkSize} bytes, got ${bytesRead}`);
|
|
279
|
+
}
|
|
280
|
+
const etag = await uploadPart(tosUrl, partNumber, uploadId, buffer, credentials, region);
|
|
281
|
+
completedParts.push({ partNumber, etag });
|
|
282
|
+
saveResumeState(resumePath, { uploadId, fileSize, parts: completedParts });
|
|
283
|
+
uploadedBytes = Math.min(offset + chunkSize, fileSize);
|
|
284
|
+
if (onProgress)
|
|
285
|
+
onProgress(uploadedBytes, fileSize);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
fs.closeSync(fd);
|
|
290
|
+
}
|
|
291
|
+
await completeMultipartUpload(tosUrl, uploadId, completedParts, credentials, region);
|
|
292
|
+
deleteResumeState(resumePath);
|
|
293
|
+
}
|
|
294
|
+
// ── Internal exports for testing ─────────────────────────────────────────────
|
|
295
|
+
export { PART_SIZE, RESUME_DIR, extractRegionFromHost, getResumeFilePath, loadResumeState, saveResumeState, deleteResumeState, computeAws4Headers, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
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 } from 'vitest';
|
|
5
|
+
import { PART_SIZE, computeAws4Headers, deleteResumeState, extractRegionFromHost, getResumeFilePath, loadResumeState, saveResumeState, } from './tos-upload.js';
|
|
6
|
+
// ── extractRegionFromHost ────────────────────────────────────────────────────
|
|
7
|
+
describe('extractRegionFromHost', () => {
|
|
8
|
+
it('extracts region from standard TOS host', () => {
|
|
9
|
+
expect(extractRegionFromHost('tos-cn-i-alisg.volces.com')).toBe('cn-i-alisg');
|
|
10
|
+
});
|
|
11
|
+
it('extracts region from beijing TOS host', () => {
|
|
12
|
+
expect(extractRegionFromHost('tos-cn-beijing.ivolces.com')).toBe('cn-beijing');
|
|
13
|
+
});
|
|
14
|
+
it('falls back to cn-north-1 for unknown host', () => {
|
|
15
|
+
expect(extractRegionFromHost('unknown.example.com')).toBe('cn-north-1');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
// ── Part chunking ────────────────────────────────────────────────────────────
|
|
19
|
+
describe('PART_SIZE and part chunking logic', () => {
|
|
20
|
+
it('PART_SIZE is exactly 5 MB', () => {
|
|
21
|
+
expect(PART_SIZE).toBe(5 * 1024 * 1024);
|
|
22
|
+
});
|
|
23
|
+
it('single file smaller than PART_SIZE fits in 1 part', () => {
|
|
24
|
+
const fileSize = 1 * 1024 * 1024; // 1 MB
|
|
25
|
+
const totalParts = Math.ceil(fileSize / PART_SIZE);
|
|
26
|
+
expect(totalParts).toBe(1);
|
|
27
|
+
const lastPartSize = fileSize - (totalParts - 1) * PART_SIZE;
|
|
28
|
+
expect(lastPartSize).toBe(fileSize);
|
|
29
|
+
});
|
|
30
|
+
it('exactly 5 MB file produces 1 part', () => {
|
|
31
|
+
const fileSize = PART_SIZE;
|
|
32
|
+
expect(Math.ceil(fileSize / PART_SIZE)).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
it('5 MB + 1 byte produces 2 parts', () => {
|
|
35
|
+
const fileSize = PART_SIZE + 1;
|
|
36
|
+
expect(Math.ceil(fileSize / PART_SIZE)).toBe(2);
|
|
37
|
+
});
|
|
38
|
+
it('100 MB file produces 20 parts of 5 MB each', () => {
|
|
39
|
+
const fileSize = 100 * 1024 * 1024;
|
|
40
|
+
const totalParts = Math.ceil(fileSize / PART_SIZE);
|
|
41
|
+
expect(totalParts).toBe(20);
|
|
42
|
+
// Each part is exactly PART_SIZE
|
|
43
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
44
|
+
const offset = (i - 1) * PART_SIZE;
|
|
45
|
+
const chunkSize = Math.min(PART_SIZE, fileSize - offset);
|
|
46
|
+
expect(chunkSize).toBe(PART_SIZE);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it('101 MB file produces 21 parts, last part is 1 MB', () => {
|
|
50
|
+
const fileSize = 101 * 1024 * 1024;
|
|
51
|
+
const totalParts = Math.ceil(fileSize / PART_SIZE);
|
|
52
|
+
expect(totalParts).toBe(21);
|
|
53
|
+
const lastOffset = (totalParts - 1) * PART_SIZE;
|
|
54
|
+
const lastPartSize = fileSize - lastOffset;
|
|
55
|
+
expect(lastPartSize).toBe(1 * 1024 * 1024);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
// ── Resume file serialization/deserialization ─────────────────────────────────
|
|
59
|
+
describe('resume state read/write', () => {
|
|
60
|
+
let tmpDir;
|
|
61
|
+
let resumePath;
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tos-upload-test-'));
|
|
64
|
+
resumePath = path.join(tmpDir, 'resume.json');
|
|
65
|
+
});
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
it('saves and loads resume state correctly', () => {
|
|
70
|
+
const state = {
|
|
71
|
+
uploadId: 'test-upload-id-123',
|
|
72
|
+
fileSize: 12345678,
|
|
73
|
+
parts: [
|
|
74
|
+
{ partNumber: 1, etag: '"abc123"' },
|
|
75
|
+
{ partNumber: 2, etag: '"def456"' },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
saveResumeState(resumePath, state);
|
|
79
|
+
const loaded = loadResumeState(resumePath, 12345678);
|
|
80
|
+
expect(loaded).not.toBeNull();
|
|
81
|
+
expect(loaded.uploadId).toBe('test-upload-id-123');
|
|
82
|
+
expect(loaded.fileSize).toBe(12345678);
|
|
83
|
+
expect(loaded.parts).toHaveLength(2);
|
|
84
|
+
expect(loaded.parts[0]).toEqual({ partNumber: 1, etag: '"abc123"' });
|
|
85
|
+
expect(loaded.parts[1]).toEqual({ partNumber: 2, etag: '"def456"' });
|
|
86
|
+
});
|
|
87
|
+
it('returns null when file does not exist', () => {
|
|
88
|
+
const result = loadResumeState('/nonexistent/path/resume.json', 12345678);
|
|
89
|
+
expect(result).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
it('returns null when fileSize does not match', () => {
|
|
92
|
+
const state = {
|
|
93
|
+
uploadId: 'upload-id',
|
|
94
|
+
fileSize: 100,
|
|
95
|
+
parts: [],
|
|
96
|
+
};
|
|
97
|
+
saveResumeState(resumePath, state);
|
|
98
|
+
// Different file size — should not resume
|
|
99
|
+
const result = loadResumeState(resumePath, 999);
|
|
100
|
+
expect(result).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
it('returns null when JSON is malformed', () => {
|
|
103
|
+
fs.writeFileSync(resumePath, 'not-valid-json', 'utf8');
|
|
104
|
+
const result = loadResumeState(resumePath, 100);
|
|
105
|
+
expect(result).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
it('returns null when uploadId is missing', () => {
|
|
108
|
+
const broken = { fileSize: 100, parts: [] };
|
|
109
|
+
fs.writeFileSync(resumePath, JSON.stringify(broken), 'utf8');
|
|
110
|
+
const result = loadResumeState(resumePath, 100);
|
|
111
|
+
expect(result).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
it('deletes resume file without throwing when file exists', () => {
|
|
114
|
+
fs.writeFileSync(resumePath, '{}', 'utf8');
|
|
115
|
+
expect(() => deleteResumeState(resumePath)).not.toThrow();
|
|
116
|
+
expect(fs.existsSync(resumePath)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it('deleteResumeState does not throw when file does not exist', () => {
|
|
119
|
+
expect(() => deleteResumeState('/nonexistent/path/resume.json')).not.toThrow();
|
|
120
|
+
});
|
|
121
|
+
it('saveResumeState creates parent directories if missing', () => {
|
|
122
|
+
const nestedPath = path.join(tmpDir, 'nested', 'deep', 'resume.json');
|
|
123
|
+
const state = { uploadId: 'x', fileSize: 0, parts: [] };
|
|
124
|
+
expect(() => saveResumeState(nestedPath, state)).not.toThrow();
|
|
125
|
+
expect(fs.existsSync(nestedPath)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ── getResumeFilePath ────────────────────────────────────────────────────────
|
|
129
|
+
describe('getResumeFilePath', () => {
|
|
130
|
+
it('returns a path inside ~/.opencli/douyin-resume/', () => {
|
|
131
|
+
const result = getResumeFilePath('/some/video/file.mp4');
|
|
132
|
+
expect(result).toContain('douyin-resume');
|
|
133
|
+
expect(result).toMatch(/\.json$/);
|
|
134
|
+
});
|
|
135
|
+
it('produces same path for same input', () => {
|
|
136
|
+
const a = getResumeFilePath('/video.mp4');
|
|
137
|
+
const b = getResumeFilePath('/video.mp4');
|
|
138
|
+
expect(a).toBe(b);
|
|
139
|
+
});
|
|
140
|
+
it('produces different paths for different inputs', () => {
|
|
141
|
+
const a = getResumeFilePath('/video1.mp4');
|
|
142
|
+
const b = getResumeFilePath('/video2.mp4');
|
|
143
|
+
expect(a).not.toBe(b);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// ── computeAws4Headers ───────────────────────────────────────────────────────
|
|
147
|
+
describe('computeAws4Headers', () => {
|
|
148
|
+
const mockCredentials = {
|
|
149
|
+
access_key_id: 'AKIAIOSFODNN7EXAMPLE',
|
|
150
|
+
secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
151
|
+
session_token: 'FQoGZXIvYXdzEJr//////////test-session-token',
|
|
152
|
+
expired_time: Date.now() / 1000 + 3600,
|
|
153
|
+
};
|
|
154
|
+
it('returns Authorization header', () => {
|
|
155
|
+
const headers = computeAws4Headers({
|
|
156
|
+
method: 'PUT',
|
|
157
|
+
url: 'https://tos-cn-i-alisg.volces.com/bucket/object?partNumber=1&uploadId=abc',
|
|
158
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
159
|
+
body: Buffer.from('hello'),
|
|
160
|
+
credentials: mockCredentials,
|
|
161
|
+
service: 'tos',
|
|
162
|
+
region: 'cn-i-alisg',
|
|
163
|
+
datetime: '20260325T120000Z',
|
|
164
|
+
});
|
|
165
|
+
expect(headers['Authorization']).toMatch(/^AWS4-HMAC-SHA256 Credential=/);
|
|
166
|
+
expect(headers['Authorization']).toContain('AKIAIOSFODNN7EXAMPLE/20260325/cn-i-alisg/tos/aws4_request');
|
|
167
|
+
expect(headers['Authorization']).toContain('SignedHeaders=');
|
|
168
|
+
expect(headers['Authorization']).toContain('Signature=');
|
|
169
|
+
});
|
|
170
|
+
it('includes x-amz-date header', () => {
|
|
171
|
+
const headers = computeAws4Headers({
|
|
172
|
+
method: 'PUT',
|
|
173
|
+
url: 'https://tos-cn-i-alisg.volces.com/bucket/object?partNumber=1&uploadId=abc',
|
|
174
|
+
headers: {},
|
|
175
|
+
body: Buffer.alloc(0),
|
|
176
|
+
credentials: mockCredentials,
|
|
177
|
+
service: 'tos',
|
|
178
|
+
region: 'cn-i-alisg',
|
|
179
|
+
datetime: '20260325T120000Z',
|
|
180
|
+
});
|
|
181
|
+
expect(headers['x-amz-date']).toBe('20260325T120000Z');
|
|
182
|
+
});
|
|
183
|
+
it('includes x-amz-security-token with session token', () => {
|
|
184
|
+
const headers = computeAws4Headers({
|
|
185
|
+
method: 'PUT',
|
|
186
|
+
url: 'https://tos-cn-i-alisg.volces.com/bucket/object',
|
|
187
|
+
headers: {},
|
|
188
|
+
body: '',
|
|
189
|
+
credentials: mockCredentials,
|
|
190
|
+
service: 'tos',
|
|
191
|
+
region: 'cn-i-alisg',
|
|
192
|
+
datetime: '20260325T120000Z',
|
|
193
|
+
});
|
|
194
|
+
expect(headers['x-amz-security-token']).toBe(mockCredentials.session_token);
|
|
195
|
+
});
|
|
196
|
+
it('signed headers list is sorted', () => {
|
|
197
|
+
const headers = computeAws4Headers({
|
|
198
|
+
method: 'POST',
|
|
199
|
+
url: 'https://tos-cn-i-alisg.volces.com/bucket/object?uploadId=abc',
|
|
200
|
+
headers: { 'content-type': 'application/xml' },
|
|
201
|
+
body: '<xml/>',
|
|
202
|
+
credentials: mockCredentials,
|
|
203
|
+
service: 'tos',
|
|
204
|
+
region: 'cn-i-alisg',
|
|
205
|
+
datetime: '20260325T120000Z',
|
|
206
|
+
});
|
|
207
|
+
const authHeader = headers['Authorization'];
|
|
208
|
+
const signedHeadersMatch = authHeader.match(/SignedHeaders=([^,]+)/);
|
|
209
|
+
expect(signedHeadersMatch).not.toBeNull();
|
|
210
|
+
const signedHeadersList = signedHeadersMatch[1].split(';');
|
|
211
|
+
const sorted = [...signedHeadersList].sort((a, b) => a.localeCompare(b));
|
|
212
|
+
expect(signedHeadersList).toEqual(sorted);
|
|
213
|
+
});
|
|
214
|
+
it('produces deterministic signature for same inputs', () => {
|
|
215
|
+
const opts = {
|
|
216
|
+
method: 'PUT',
|
|
217
|
+
url: 'https://tos-cn-i-alisg.volces.com/bucket/key?partNumber=1&uploadId=xyz',
|
|
218
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
219
|
+
body: Buffer.from('test-data'),
|
|
220
|
+
credentials: mockCredentials,
|
|
221
|
+
service: 'tos',
|
|
222
|
+
region: 'cn-i-alisg',
|
|
223
|
+
datetime: '20260325T120000Z',
|
|
224
|
+
};
|
|
225
|
+
const h1 = computeAws4Headers(opts);
|
|
226
|
+
const h2 = computeAws4Headers(opts);
|
|
227
|
+
expect(h1['Authorization']).toBe(h2['Authorization']);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcode poller for Douyin video processing.
|
|
3
|
+
*
|
|
4
|
+
* After a video is uploaded via TOS and the "confirm upload" API is called,
|
|
5
|
+
* Douyin transcodes the video asynchronously. This module polls the transcode
|
|
6
|
+
* status endpoint until encode=2 (complete) or a timeout is reached.
|
|
7
|
+
*/
|
|
8
|
+
import type { IPage } from '../../../types.js';
|
|
9
|
+
import type { TranscodeResult } from './types.js';
|
|
10
|
+
type BrowserFetchFn = (page: IPage, method: 'GET' | 'POST', url: string) => Promise<unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Lower-level poll function that accepts an injected fetch function.
|
|
13
|
+
* Exported for testability.
|
|
14
|
+
*/
|
|
15
|
+
export declare function pollTranscodeWithFetch(fetchFn: BrowserFetchFn, page: IPage, videoId: string, timeoutMs?: number): Promise<TranscodeResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Poll Douyin's transcode status endpoint until the video is fully transcoded
|
|
18
|
+
* (encode=2) or the timeout expires.
|
|
19
|
+
*
|
|
20
|
+
* @param page - Browser page for making credentialed API calls
|
|
21
|
+
* @param videoId - The video_id returned from the confirm upload step
|
|
22
|
+
* @param timeoutMs - Maximum wait time in ms (default: 300 000 = 5 minutes)
|
|
23
|
+
* @returns TranscodeResult including duration, fps, dimensions, and poster info
|
|
24
|
+
* @throws TimeoutError if transcode does not complete within timeoutMs
|
|
25
|
+
*/
|
|
26
|
+
export declare function pollTranscode(page: IPage, videoId: string, timeoutMs?: number): Promise<TranscodeResult>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcode poller for Douyin video processing.
|
|
3
|
+
*
|
|
4
|
+
* After a video is uploaded via TOS and the "confirm upload" API is called,
|
|
5
|
+
* Douyin transcodes the video asynchronously. This module polls the transcode
|
|
6
|
+
* status endpoint until encode=2 (complete) or a timeout is reached.
|
|
7
|
+
*/
|
|
8
|
+
import { TimeoutError } from '../../../errors.js';
|
|
9
|
+
import { browserFetch } from './browser-fetch.js';
|
|
10
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
12
|
+
const TRANSCODE_URL_BASE = 'https://creator.douyin.com/web/api/media/video/transend/';
|
|
13
|
+
/**
|
|
14
|
+
* Lower-level poll function that accepts an injected fetch function.
|
|
15
|
+
* Exported for testability.
|
|
16
|
+
*/
|
|
17
|
+
export async function pollTranscodeWithFetch(fetchFn, page, videoId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
18
|
+
const url = `${TRANSCODE_URL_BASE}?video_id=${encodeURIComponent(videoId)}&aid=1128`;
|
|
19
|
+
const deadline = Date.now() + timeoutMs;
|
|
20
|
+
while (Date.now() < deadline) {
|
|
21
|
+
const result = (await fetchFn(page, 'GET', url));
|
|
22
|
+
if (result.encode === 2) {
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
// Wait before next poll, but don't exceed the deadline
|
|
26
|
+
const remaining = deadline - Date.now();
|
|
27
|
+
if (remaining <= 0)
|
|
28
|
+
break;
|
|
29
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(POLL_INTERVAL_MS, remaining)));
|
|
30
|
+
}
|
|
31
|
+
throw new TimeoutError(`Douyin transcode for video ${videoId}`, Math.round(timeoutMs / 1000));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Poll Douyin's transcode status endpoint until the video is fully transcoded
|
|
35
|
+
* (encode=2) or the timeout expires.
|
|
36
|
+
*
|
|
37
|
+
* @param page - Browser page for making credentialed API calls
|
|
38
|
+
* @param videoId - The video_id returned from the confirm upload step
|
|
39
|
+
* @param timeoutMs - Maximum wait time in ms (default: 300 000 = 5 minutes)
|
|
40
|
+
* @returns TranscodeResult including duration, fps, dimensions, and poster info
|
|
41
|
+
* @throws TimeoutError if transcode does not complete within timeoutMs
|
|
42
|
+
*/
|
|
43
|
+
export async function pollTranscode(page, videoId, timeoutMs) {
|
|
44
|
+
return pollTranscodeWithFetch(browserFetch, page, videoId, timeoutMs);
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|