@jackwener/opencli 1.5.6 → 1.5.8
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/CHANGELOG.md +34 -0
- package/README.md +4 -2
- package/README.zh-CN.md +4 -1
- package/SKILL.md +879 -0
- package/dist/browser/cdp.d.ts +1 -0
- package/dist/browser/cdp.js +30 -27
- package/dist/browser/daemon-client.d.ts +7 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.js +1 -0
- package/dist/browser/dom-helpers.test.js +14 -1
- package/dist/browser/mcp.js +18 -13
- package/dist/browser/page.js +22 -2
- package/dist/browser/page.test.d.ts +1 -0
- package/dist/browser/page.test.js +44 -0
- package/dist/browser/stealth.js +198 -0
- package/dist/browser/stealth.test.d.ts +1 -0
- package/dist/browser/stealth.test.js +134 -0
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +5 -1
- package/dist/build-manifest.test.js +2 -0
- package/dist/cli-manifest.json +544 -137
- package/dist/cli.js +20 -3
- package/dist/clis/antigravity/serve.d.ts +1 -1
- package/dist/clis/antigravity/serve.js +5 -8
- package/dist/clis/bilibili/subtitle.js +4 -0
- package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
- package/dist/clis/bilibili/subtitle.test.js +48 -0
- package/dist/clis/chatwise/ask.js +0 -2
- package/dist/clis/chatwise/export.js +0 -2
- package/dist/clis/chatwise/history.js +0 -2
- package/dist/clis/chatwise/model.js +0 -2
- package/dist/clis/chatwise/new.js +1 -2
- package/dist/clis/chatwise/read.js +0 -2
- package/dist/clis/chatwise/screenshot.js +1 -2
- package/dist/clis/chatwise/send.js +0 -2
- package/dist/clis/chatwise/status.js +1 -2
- package/dist/clis/ctrip/search.d.ts +13 -0
- package/dist/clis/ctrip/search.js +73 -48
- package/dist/clis/ctrip/search.test.d.ts +1 -0
- package/dist/clis/ctrip/search.test.js +64 -0
- package/dist/clis/douyin/_shared/sts2.js +8 -2
- package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/sts2.test.js +27 -0
- package/dist/clis/douyin/activities.js +4 -2
- package/dist/clis/douyin/activities.test.js +34 -1
- package/dist/clis/douyin/collections.js +1 -1
- package/dist/clis/douyin/collections.test.js +24 -2
- package/dist/clis/douyin/draft.d.ts +8 -11
- package/dist/clis/douyin/draft.js +302 -185
- package/dist/clis/douyin/draft.test.d.ts +1 -1
- package/dist/clis/douyin/draft.test.js +357 -2
- package/dist/clis/douyin/hashtag.js +9 -2
- package/dist/clis/douyin/hashtag.test.js +35 -2
- package/dist/clis/douyin/profile.js +1 -1
- package/dist/clis/douyin/profile.test.js +36 -1
- package/dist/clis/douyin/videos.js +22 -5
- package/dist/clis/douyin/videos.test.js +45 -2
- package/dist/clis/facebook/search.test.d.ts +5 -0
- package/dist/clis/facebook/search.test.js +60 -0
- package/dist/clis/facebook/search.yaml +4 -3
- package/dist/clis/instagram/download.d.ts +16 -0
- package/dist/clis/instagram/download.js +225 -0
- package/dist/clis/instagram/download.test.d.ts +1 -0
- package/dist/clis/instagram/download.test.js +118 -0
- package/dist/clis/notebooklm/bind-current.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.js +29 -0
- package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.test.js +35 -0
- package/dist/clis/notebooklm/binding.test.d.ts +1 -0
- package/dist/clis/notebooklm/binding.test.js +44 -0
- package/dist/clis/notebooklm/compat.test.d.ts +3 -0
- package/dist/clis/notebooklm/compat.test.js +16 -0
- package/dist/clis/notebooklm/current.d.ts +1 -0
- package/dist/clis/notebooklm/current.js +28 -0
- package/dist/clis/notebooklm/get.d.ts +1 -0
- package/dist/clis/notebooklm/get.js +37 -0
- package/dist/clis/notebooklm/history.d.ts +1 -0
- package/dist/clis/notebooklm/history.js +25 -0
- package/dist/clis/notebooklm/history.test.d.ts +1 -0
- package/dist/clis/notebooklm/history.test.js +58 -0
- package/dist/clis/notebooklm/list.d.ts +1 -0
- package/dist/clis/notebooklm/list.js +35 -0
- package/dist/clis/notebooklm/note-list.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.js +28 -0
- package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.test.js +56 -0
- package/dist/clis/notebooklm/notes-get.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.js +47 -0
- package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.test.js +72 -0
- package/dist/clis/notebooklm/rpc.d.ts +36 -0
- package/dist/clis/notebooklm/rpc.js +189 -0
- package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
- package/dist/clis/notebooklm/rpc.test.js +105 -0
- package/dist/clis/notebooklm/shared.d.ts +87 -0
- package/dist/clis/notebooklm/shared.js +3 -0
- package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.js +44 -0
- package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
- package/dist/clis/notebooklm/source-get.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.js +40 -0
- package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.test.js +84 -0
- package/dist/clis/notebooklm/source-guide.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.js +44 -0
- package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.test.js +104 -0
- package/dist/clis/notebooklm/source-list.d.ts +1 -0
- package/dist/clis/notebooklm/source-list.js +30 -0
- package/dist/clis/notebooklm/status.d.ts +1 -0
- package/dist/clis/notebooklm/status.js +31 -0
- package/dist/clis/notebooklm/summary.d.ts +1 -0
- package/dist/clis/notebooklm/summary.js +30 -0
- package/dist/clis/notebooklm/summary.test.d.ts +1 -0
- package/dist/clis/notebooklm/summary.test.js +78 -0
- package/dist/clis/notebooklm/utils.d.ts +37 -0
- package/dist/clis/notebooklm/utils.js +739 -0
- package/dist/clis/notebooklm/utils.test.d.ts +1 -0
- package/dist/clis/notebooklm/utils.test.js +390 -0
- package/dist/clis/substack/utils.d.ts +4 -0
- package/dist/clis/substack/utils.js +8 -2
- package/dist/clis/substack/utils.test.d.ts +1 -0
- package/dist/clis/substack/utils.test.js +46 -0
- package/dist/clis/v2ex/hot.yaml +4 -1
- package/dist/clis/v2ex/latest.yaml +4 -1
- package/dist/clis/v2ex/topic.yaml +6 -1
- package/dist/clis/weixin/download.d.ts +9 -0
- package/dist/clis/weixin/download.js +76 -6
- package/dist/clis/weread/book.js +108 -2
- package/dist/clis/weread/commands.test.js +262 -152
- package/dist/clis/weread/utils.d.ts +10 -0
- package/dist/clis/weread/utils.js +27 -7
- package/dist/clis/xiaohongshu/comments.d.ts +3 -0
- package/dist/clis/xiaohongshu/comments.js +76 -17
- package/dist/clis/xiaohongshu/comments.test.js +70 -9
- package/dist/clis/xiaohongshu/download.d.ts +4 -1
- package/dist/clis/xiaohongshu/download.js +83 -22
- package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/download.test.js +75 -0
- package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
- package/dist/clis/xiaohongshu/note-helpers.js +23 -0
- package/dist/clis/xiaohongshu/note.d.ts +7 -0
- package/dist/clis/xiaohongshu/note.js +76 -0
- package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/note.test.js +136 -0
- package/dist/clis/xiaohongshu/search.js +9 -0
- package/dist/clis/xiaohongshu/search.test.js +10 -4
- package/dist/clis/youtube/search.js +57 -17
- package/dist/clis/zhihu/question.js +19 -17
- package/dist/clis/zhihu/question.test.d.ts +1 -0
- package/dist/clis/zhihu/question.test.js +54 -0
- package/dist/commanderAdapter.js +9 -0
- package/dist/commanderAdapter.test.js +25 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +124 -0
- package/dist/commands/daemon.test.d.ts +1 -0
- package/dist/commands/daemon.test.js +185 -0
- package/dist/completion.js +3 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +25 -14
- package/dist/daemon.test.d.ts +1 -0
- package/dist/daemon.test.js +65 -0
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +47 -2
- package/dist/electron-apps.d.ts +29 -0
- package/dist/electron-apps.js +65 -0
- package/dist/electron-apps.test.d.ts +1 -0
- package/dist/electron-apps.test.js +43 -0
- package/dist/engine.test.js +41 -9
- package/dist/execution.js +20 -16
- package/dist/extension-manifest-regression.test.js +1 -0
- package/dist/idle-manager.d.ts +19 -0
- package/dist/idle-manager.js +54 -0
- package/dist/launcher.d.ts +36 -0
- package/dist/launcher.js +152 -0
- package/dist/launcher.test.d.ts +1 -0
- package/dist/launcher.test.js +57 -0
- package/dist/main.js +3 -3
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +31 -3
- package/dist/registry.test.js +13 -0
- package/dist/runtime.d.ts +5 -3
- package/dist/runtime.js +12 -5
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +3 -0
- package/dist/serialization.test.js +17 -1
- package/dist/tui.d.ts +7 -0
- package/dist/tui.js +52 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +19 -0
- package/dist/weixin-download.test.js +14 -0
- package/docs/.vitepress/config.mts +1 -0
- package/docs/adapters/browser/notebooklm.md +69 -0
- package/docs/adapters/browser/xiaohongshu.md +19 -10
- package/docs/adapters/index.md +67 -66
- package/docs/guide/browser-bridge.md +12 -0
- package/docs/guide/troubleshooting.md +9 -4
- package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
- package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
- package/docs/zh/guide/browser-bridge.md +12 -0
- package/extension/dist/background.js +250 -11
- package/extension/manifest.json +2 -1
- package/extension/src/background.test.ts +202 -2
- package/extension/src/background.ts +175 -10
- package/extension/src/cdp.test.ts +75 -0
- package/extension/src/cdp.ts +89 -3
- package/extension/src/protocol.ts +7 -5
- package/package.json +1 -1
- package/src/browser/cdp.ts +24 -17
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.test.ts +15 -1
- package/src/browser/dom-helpers.ts +1 -0
- package/src/browser/mcp.ts +18 -13
- package/src/browser/page.test.ts +58 -0
- package/src/browser/page.ts +18 -2
- package/src/browser/stealth.test.ts +153 -0
- package/src/browser/stealth.ts +198 -0
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.test.ts +2 -0
- package/src/build-manifest.ts +6 -1
- package/src/cli.ts +21 -3
- package/src/clis/antigravity/SKILL.md +3 -12
- package/src/clis/antigravity/serve.ts +5 -10
- package/src/clis/bilibili/subtitle.test.ts +60 -0
- package/src/clis/bilibili/subtitle.ts +4 -0
- package/src/clis/chatwise/ask.ts +0 -2
- package/src/clis/chatwise/export.ts +0 -2
- package/src/clis/chatwise/history.ts +0 -2
- package/src/clis/chatwise/model.ts +0 -2
- package/src/clis/chatwise/new.ts +1 -2
- package/src/clis/chatwise/read.ts +0 -2
- package/src/clis/chatwise/screenshot.ts +1 -2
- package/src/clis/chatwise/send.ts +0 -2
- package/src/clis/chatwise/status.ts +1 -2
- package/src/clis/ctrip/search.test.ts +73 -0
- package/src/clis/ctrip/search.ts +97 -47
- package/src/clis/douyin/_shared/sts2.test.ts +31 -0
- package/src/clis/douyin/_shared/sts2.ts +11 -3
- package/src/clis/douyin/activities.test.ts +41 -1
- package/src/clis/douyin/activities.ts +12 -3
- package/src/clis/douyin/collections.test.ts +35 -2
- package/src/clis/douyin/collections.ts +1 -1
- package/src/clis/douyin/draft.test.ts +444 -2
- package/src/clis/douyin/draft.ts +382 -218
- package/src/clis/douyin/hashtag.test.ts +42 -2
- package/src/clis/douyin/hashtag.ts +11 -3
- package/src/clis/douyin/profile.test.ts +43 -1
- package/src/clis/douyin/profile.ts +9 -2
- package/src/clis/douyin/videos.test.ts +52 -2
- package/src/clis/douyin/videos.ts +49 -15
- package/src/clis/facebook/search.test.ts +70 -0
- package/src/clis/facebook/search.yaml +4 -3
- package/src/clis/instagram/download.test.ts +159 -0
- package/src/clis/instagram/download.ts +286 -0
- package/src/clis/notebooklm/bind-current.test.ts +43 -0
- package/src/clis/notebooklm/bind-current.ts +36 -0
- package/src/clis/notebooklm/binding.test.ts +53 -0
- package/src/clis/notebooklm/compat.test.ts +19 -0
- package/src/clis/notebooklm/current.ts +38 -0
- package/src/clis/notebooklm/get.ts +53 -0
- package/src/clis/notebooklm/history.test.ts +70 -0
- package/src/clis/notebooklm/history.ts +36 -0
- package/src/clis/notebooklm/list.ts +40 -0
- package/src/clis/notebooklm/note-list.test.ts +64 -0
- package/src/clis/notebooklm/note-list.ts +42 -0
- package/src/clis/notebooklm/notes-get.test.ts +88 -0
- package/src/clis/notebooklm/notes-get.ts +67 -0
- package/src/clis/notebooklm/rpc.test.ts +126 -0
- package/src/clis/notebooklm/rpc.ts +286 -0
- package/src/clis/notebooklm/shared.ts +98 -0
- package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
- package/src/clis/notebooklm/source-fulltext.ts +69 -0
- package/src/clis/notebooklm/source-get.test.ts +100 -0
- package/src/clis/notebooklm/source-get.ts +60 -0
- package/src/clis/notebooklm/source-guide.test.ts +121 -0
- package/src/clis/notebooklm/source-guide.ts +69 -0
- package/src/clis/notebooklm/source-list.ts +45 -0
- package/src/clis/notebooklm/status.ts +34 -0
- package/src/clis/notebooklm/summary.test.ts +94 -0
- package/src/clis/notebooklm/summary.ts +45 -0
- package/src/clis/notebooklm/utils.test.ts +446 -0
- package/src/clis/notebooklm/utils.ts +893 -0
- package/src/clis/substack/utils.test.ts +54 -0
- package/src/clis/substack/utils.ts +10 -2
- package/src/clis/v2ex/hot.yaml +4 -1
- package/src/clis/v2ex/latest.yaml +4 -1
- package/src/clis/v2ex/topic.yaml +6 -1
- package/src/clis/weixin/download.ts +95 -6
- package/src/clis/weread/book.ts +142 -2
- package/src/clis/weread/commands.test.ts +314 -154
- package/src/clis/weread/utils.ts +33 -4
- package/src/clis/xiaohongshu/comments.test.ts +85 -9
- package/src/clis/xiaohongshu/comments.ts +76 -17
- package/src/clis/xiaohongshu/download.test.ts +96 -0
- package/src/clis/xiaohongshu/download.ts +83 -22
- package/src/clis/xiaohongshu/note-helpers.ts +25 -0
- package/src/clis/xiaohongshu/note.test.ts +164 -0
- package/src/clis/xiaohongshu/note.ts +86 -0
- package/src/clis/xiaohongshu/search.test.ts +11 -4
- package/src/clis/xiaohongshu/search.ts +13 -0
- package/src/clis/youtube/search.ts +57 -17
- package/src/clis/zhihu/question.test.ts +71 -0
- package/src/clis/zhihu/question.ts +27 -15
- package/src/commanderAdapter.test.ts +30 -0
- package/src/commanderAdapter.ts +7 -0
- package/src/commands/daemon.test.ts +238 -0
- package/src/commands/daemon.ts +135 -0
- package/src/completion.ts +2 -1
- package/src/constants.ts +3 -0
- package/src/daemon.test.ts +88 -0
- package/src/daemon.ts +26 -14
- package/src/discovery.ts +52 -2
- package/src/electron-apps.test.ts +50 -0
- package/src/electron-apps.ts +89 -0
- package/src/engine.test.ts +45 -9
- package/src/execution.ts +24 -19
- package/src/extension-manifest-regression.test.ts +1 -0
- package/src/idle-manager.ts +60 -0
- package/src/launcher.test.ts +67 -0
- package/src/launcher.ts +185 -0
- package/src/main.ts +3 -2
- package/src/registry.test.ts +15 -0
- package/src/registry.ts +32 -3
- package/src/runtime.ts +13 -7
- package/src/serialization.test.ts +19 -1
- package/src/serialization.ts +2 -0
- package/src/tui.test.ts +23 -0
- package/src/tui.ts +65 -0
- package/src/weixin-download.test.ts +27 -0
- package/tests/e2e/browser-public-extended.test.ts +6 -2
- package/chatwise-opencli.ps1 +0 -82
- package/dist/clis/chatwise/shared.d.ts +0 -2
- package/dist/clis/chatwise/shared.js +0 -6
- package/src/clis/chatwise/shared.ts +0 -8
package/src/clis/douyin/draft.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Douyin draft —
|
|
2
|
+
* Douyin draft — upload through the official creator page and save as draft.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 3. TOS multipart upload
|
|
8
|
-
* 4. Cover upload (optional, via ImageX)
|
|
9
|
-
* 5. Enable video
|
|
10
|
-
* 6. Poll transcode
|
|
11
|
-
* 7. (skipped — no safety check for drafts)
|
|
12
|
-
* 8. create_v2 with is_draft: 1
|
|
4
|
+
* The previous API pipeline relied on an old pre-upload endpoint that no longer
|
|
5
|
+
* matches creator center's live upload flow. This command now drives the
|
|
6
|
+
* official upload page directly so it stays aligned with the site.
|
|
13
7
|
*/
|
|
14
8
|
|
|
15
9
|
import * as fs from 'node:fs';
|
|
@@ -17,51 +11,362 @@ import * as path from 'node:path';
|
|
|
17
11
|
import { cli, Strategy } from '../../registry.js';
|
|
18
12
|
import { ArgumentError, CommandExecutionError } from '../../errors.js';
|
|
19
13
|
import type { IPage } from '../../types.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import { browserFetch } from './_shared/browser-fetch.js';
|
|
26
|
-
import { generateCreationId } from './_shared/creation-id.js';
|
|
27
|
-
import { parseTextExtra, extractHashtagNames } from './_shared/text-extra.js';
|
|
28
|
-
import type { HashtagInfo } from './_shared/text-extra.js';
|
|
29
|
-
|
|
30
|
-
const VISIBILITY_MAP: Record<string, number> = {
|
|
31
|
-
public: 0,
|
|
32
|
-
friends: 1,
|
|
33
|
-
private: 2,
|
|
14
|
+
|
|
15
|
+
const VISIBILITY_LABELS: Record<string, string> = {
|
|
16
|
+
public: '公开',
|
|
17
|
+
friends: '好友可见',
|
|
18
|
+
private: '仅自己可见',
|
|
34
19
|
};
|
|
35
20
|
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
21
|
+
const DRAFT_UPLOAD_URL = 'https://creator.douyin.com/creator-micro/content/upload';
|
|
22
|
+
const COMPOSER_WAIT_ATTEMPTS = 120;
|
|
23
|
+
const COVER_INPUT_WAIT_ATTEMPTS = 20;
|
|
24
|
+
const COVER_READY_WAIT_ATTEMPTS = 20;
|
|
25
|
+
|
|
26
|
+
interface DraftComposerState {
|
|
27
|
+
href: string;
|
|
28
|
+
ready: boolean;
|
|
29
|
+
bodyText: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DraftSaveResult {
|
|
33
|
+
text: string;
|
|
34
|
+
creationId: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface DraftResumeState {
|
|
38
|
+
href: string;
|
|
39
|
+
bodyText: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Best-effort dismissal for coach marks and upload tips that can block clicks.
|
|
44
|
+
*/
|
|
45
|
+
async function dismissKnownModals(page: IPage): Promise<void> {
|
|
46
|
+
await page.evaluate(`() => {
|
|
47
|
+
const targets = ['我知道了', '知道了', '关闭'];
|
|
48
|
+
for (const text of targets) {
|
|
49
|
+
const btn = Array.from(document.querySelectorAll('button,[role="button"]'))
|
|
50
|
+
.find((el) => (el.textContent || '').trim() === text);
|
|
51
|
+
if (btn instanceof HTMLElement) btn.click();
|
|
52
|
+
}
|
|
53
|
+
}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Wait until Douyin finishes uploading and lands on the post-video composer.
|
|
58
|
+
*/
|
|
59
|
+
async function waitForDraftComposer(page: IPage): Promise<void> {
|
|
60
|
+
let lastState: DraftComposerState = {
|
|
61
|
+
href: '',
|
|
62
|
+
ready: false,
|
|
63
|
+
bodyText: '',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (let attempt = 0; attempt < COMPOSER_WAIT_ATTEMPTS; attempt += 1) {
|
|
67
|
+
lastState = (await page.evaluate(`() => ({
|
|
68
|
+
href: location.href,
|
|
69
|
+
ready: !!Array.from(document.querySelectorAll('input')).find(
|
|
70
|
+
(el) => (el.placeholder || '').includes('填写作品标题')
|
|
71
|
+
) && !!Array.from(document.querySelectorAll('button')).find(
|
|
72
|
+
(el) => (el.textContent || '').includes('暂存离开')
|
|
73
|
+
),
|
|
74
|
+
bodyText: document.body?.innerText || ''
|
|
75
|
+
})`)) as DraftComposerState;
|
|
76
|
+
if (lastState.ready) return;
|
|
77
|
+
await page.wait({ time: 0.5 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw new CommandExecutionError(
|
|
81
|
+
'等待抖音草稿编辑页超时',
|
|
82
|
+
`当前页面: ${lastState.href || 'unknown'}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fill title, caption and visibility controls on the live composer page.
|
|
88
|
+
*/
|
|
89
|
+
async function fillDraftComposer(
|
|
90
|
+
page: IPage,
|
|
91
|
+
options: { title: string; caption: string; visibilityLabel: string },
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const titleOk = (await page.evaluate(`() => {
|
|
94
|
+
const titleInput = Array.from(document.querySelectorAll('input')).find(
|
|
95
|
+
(el) => (el.placeholder || '').includes('填写作品标题')
|
|
96
|
+
);
|
|
97
|
+
if (!(titleInput instanceof HTMLInputElement)) return false;
|
|
98
|
+
const propKey = Object.keys(titleInput).find((key) => key.startsWith('__reactProps$'));
|
|
99
|
+
const props = propKey ? titleInput[propKey] : null;
|
|
100
|
+
if (props?.onChange) {
|
|
101
|
+
props.onChange({
|
|
102
|
+
target: { value: ${JSON.stringify(options.title)} },
|
|
103
|
+
currentTarget: { value: ${JSON.stringify(options.title)} },
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
titleInput.focus();
|
|
107
|
+
titleInput.value = ${JSON.stringify(options.title)};
|
|
108
|
+
titleInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
109
|
+
titleInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
110
|
+
}
|
|
111
|
+
if (props?.onBlur) {
|
|
112
|
+
props.onBlur({
|
|
113
|
+
target: titleInput,
|
|
114
|
+
currentTarget: titleInput,
|
|
115
|
+
relatedTarget: null,
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
titleInput.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}`)) as boolean;
|
|
122
|
+
if (!titleOk) {
|
|
123
|
+
throw new CommandExecutionError(
|
|
124
|
+
'填写抖音草稿表单失败: title-input-missing',
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.caption) {
|
|
129
|
+
const captionOk = (await page.evaluate(`() => {
|
|
130
|
+
const editor = document.querySelector('[contenteditable="true"]');
|
|
131
|
+
if (!(editor instanceof HTMLElement)) return false;
|
|
132
|
+
editor.focus();
|
|
133
|
+
editor.textContent = '';
|
|
134
|
+
document.execCommand('selectAll', false);
|
|
135
|
+
document.execCommand('insertText', false, ${JSON.stringify(options.caption)});
|
|
136
|
+
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
|
137
|
+
return true;
|
|
138
|
+
}`)) as boolean;
|
|
139
|
+
if (!captionOk) {
|
|
140
|
+
throw new CommandExecutionError(
|
|
141
|
+
'填写抖音草稿表单失败: caption-editor-missing',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const visibilityOk = (await page.evaluate(`() => {
|
|
147
|
+
const visibility = Array.from(document.querySelectorAll('label')).find(
|
|
148
|
+
(el) => (el.textContent || '').includes(${JSON.stringify(options.visibilityLabel)})
|
|
149
|
+
);
|
|
150
|
+
if (!(visibility instanceof HTMLElement)) return false;
|
|
151
|
+
visibility.click();
|
|
152
|
+
return true;
|
|
153
|
+
}`)) as boolean;
|
|
154
|
+
if (!visibilityOk) {
|
|
155
|
+
throw new CommandExecutionError(
|
|
156
|
+
'填写抖音草稿表单失败: visibility-missing',
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Switch the composer into custom-cover mode and expose the cover input with a
|
|
163
|
+
* stable selector for CDP file injection.
|
|
164
|
+
*/
|
|
165
|
+
async function prepareCustomCoverInput(page: IPage): Promise<string> {
|
|
166
|
+
let lastReason = 'cover-input-missing';
|
|
167
|
+
const baselineCount = (await page.evaluate(
|
|
168
|
+
`() => Array.from(document.querySelectorAll('input[type="file"]')).length`,
|
|
169
|
+
)) as number;
|
|
170
|
+
|
|
171
|
+
for (let attempt = 0; attempt < COVER_INPUT_WAIT_ATTEMPTS; attempt += 1) {
|
|
172
|
+
const result = (await page.evaluate(`() => {
|
|
173
|
+
const coverLabel = Array.from(document.querySelectorAll('label')).find(
|
|
174
|
+
(el) => (el.textContent || '').includes('上传新封面')
|
|
175
|
+
);
|
|
176
|
+
if (coverLabel instanceof HTMLElement) {
|
|
177
|
+
coverLabel.click();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
181
|
+
const target = inputs
|
|
182
|
+
.slice(${JSON.stringify(baselineCount)})
|
|
183
|
+
.find((el) => el instanceof HTMLInputElement && !el.disabled);
|
|
184
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
185
|
+
return { ok: false, reason: 'cover-input-pending' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
document
|
|
189
|
+
.querySelectorAll('[data-opencli-cover-input="1"]')
|
|
190
|
+
.forEach((el) => el.removeAttribute('data-opencli-cover-input'));
|
|
191
|
+
target.setAttribute('data-opencli-cover-input', '1');
|
|
192
|
+
return { ok: true, selector: '[data-opencli-cover-input="1"]' };
|
|
193
|
+
}`)) as { ok?: boolean; reason?: string; selector?: string };
|
|
194
|
+
|
|
195
|
+
if (result?.ok && result.selector) {
|
|
196
|
+
return result.selector;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
lastReason = result?.reason || lastReason;
|
|
200
|
+
await page.wait({ time: 0.5 });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw new CommandExecutionError(
|
|
204
|
+
`准备抖音自定义封面输入框失败: ${lastReason}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Read the local quick-check panel text that reflects cover validation state.
|
|
210
|
+
*/
|
|
211
|
+
export function buildCoverCheckPanelTextJs(): string {
|
|
212
|
+
return `() => {
|
|
213
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
214
|
+
const stateTexts = ['检测', '检测中', '封面检测中', '重新检测', '横/竖双封面缺失'];
|
|
215
|
+
const marker = Array.from(document.querySelectorAll('div,span,p,button')).find(
|
|
216
|
+
(el) => normalize(el.textContent) === '快速检测'
|
|
217
|
+
);
|
|
218
|
+
let root = marker?.parentElement || null;
|
|
219
|
+
while (root && root !== document.body) {
|
|
220
|
+
const descendants = Array.from(root.querySelectorAll('div,span,p,button'))
|
|
221
|
+
.map((el) => normalize(el.textContent));
|
|
222
|
+
const hasMarkerText = descendants.includes('快速检测');
|
|
223
|
+
const hasStateText = descendants.some((text) => stateTexts.includes(text));
|
|
224
|
+
if (hasMarkerText && hasStateText) {
|
|
225
|
+
return normalize(root.textContent).slice(0, 400);
|
|
226
|
+
}
|
|
227
|
+
root = root.parentElement;
|
|
228
|
+
}
|
|
229
|
+
return '';
|
|
230
|
+
}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function getCoverCheckPanelText(page: IPage): Promise<string> {
|
|
234
|
+
return ((await page.evaluate(buildCoverCheckPanelTextJs())) as string) || '';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Wait for Douyin's cover-detection pipeline to expose a post-upload signal.
|
|
239
|
+
* In the live creator page, custom cover upload first shows `封面检测中`, then
|
|
240
|
+
* lands on a ready state such as `重新检测` or the warning copy for missing
|
|
241
|
+
* horizontal/vertical covers.
|
|
242
|
+
*/
|
|
243
|
+
async function waitForCoverReady(
|
|
244
|
+
page: IPage,
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
let lastPanelText = '';
|
|
247
|
+
let sawBusy = false;
|
|
248
|
+
|
|
249
|
+
for (let attempt = 0; attempt < COVER_READY_WAIT_ATTEMPTS; attempt += 1) {
|
|
250
|
+
const panelText = await getCoverCheckPanelText(page);
|
|
251
|
+
const busy = panelText.includes('检测中');
|
|
252
|
+
const ready = (
|
|
253
|
+
panelText.includes('重新检测')
|
|
254
|
+
|| panelText.includes('横/竖双封面缺失')
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (busy) {
|
|
258
|
+
sawBusy = true;
|
|
259
|
+
}
|
|
260
|
+
if (sawBusy && ready && !busy) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
lastPanelText = panelText;
|
|
265
|
+
await page.wait({ time: 0.5 });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw new CommandExecutionError(
|
|
269
|
+
'等待抖音封面处理完成超时',
|
|
270
|
+
lastPanelText || 'unknown',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Click the draft button on the composer page and extract the current creation id.
|
|
276
|
+
*/
|
|
277
|
+
async function clickSaveDraft(page: IPage): Promise<DraftSaveResult> {
|
|
278
|
+
const result = (await page.evaluate(`() => {
|
|
279
|
+
const extractCreationId = () => {
|
|
280
|
+
const titleInput = Array.from(document.querySelectorAll('input')).find(
|
|
281
|
+
(el) => (el.placeholder || '').includes('填写作品标题')
|
|
282
|
+
);
|
|
283
|
+
if (!(titleInput instanceof HTMLInputElement)) return '';
|
|
284
|
+
|
|
285
|
+
const fiberKey = Object.keys(titleInput).find((key) => key.startsWith('__reactFiber$'));
|
|
286
|
+
let fiber = fiberKey ? titleInput[fiberKey] : null;
|
|
287
|
+
while (fiber) {
|
|
288
|
+
const props = fiber.memoizedProps;
|
|
289
|
+
if (typeof props?.creation_id === 'string' && props.creation_id) {
|
|
290
|
+
return props.creation_id;
|
|
291
|
+
}
|
|
292
|
+
fiber = fiber.return;
|
|
293
|
+
}
|
|
294
|
+
return '';
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const btn = Array.from(document.querySelectorAll('button')).find(
|
|
298
|
+
(el) => (el.textContent || '').includes('暂存离开')
|
|
299
|
+
);
|
|
300
|
+
if (!(btn instanceof HTMLButtonElement)) {
|
|
301
|
+
return { ok: false, reason: 'draft-button-missing' };
|
|
302
|
+
}
|
|
303
|
+
const creationId = extractCreationId();
|
|
304
|
+
const propKey = Object.keys(btn).find((key) => key.startsWith('__reactProps$'));
|
|
305
|
+
const props = propKey ? btn[propKey] : null;
|
|
306
|
+
if (props?.onClick) {
|
|
307
|
+
props.onClick({
|
|
308
|
+
preventDefault() {},
|
|
309
|
+
stopPropagation() {},
|
|
310
|
+
nativeEvent: null,
|
|
311
|
+
target: btn,
|
|
312
|
+
currentTarget: btn,
|
|
313
|
+
});
|
|
314
|
+
} else {
|
|
315
|
+
btn.click();
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
ok: true,
|
|
319
|
+
text: (btn.textContent || '').trim(),
|
|
320
|
+
creationId,
|
|
321
|
+
};
|
|
322
|
+
}`)) as { ok?: boolean; reason?: string; text?: string; creationId?: string };
|
|
323
|
+
|
|
324
|
+
if (!result?.ok) {
|
|
325
|
+
throw new CommandExecutionError(
|
|
326
|
+
`点击草稿按钮失败: ${result?.reason || 'unknown'}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (!result.creationId) {
|
|
330
|
+
throw new CommandExecutionError(
|
|
331
|
+
'点击草稿按钮失败: creation-id-missing',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
text: result.text || '暂存离开',
|
|
337
|
+
creationId: result.creationId,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Wait until creator center shows the resumable-draft prompt after saving.
|
|
343
|
+
*/
|
|
344
|
+
async function waitForDraftResult(
|
|
345
|
+
page: IPage,
|
|
346
|
+
creationId: string,
|
|
347
|
+
): Promise<string> {
|
|
348
|
+
let lastState: DraftResumeState = { href: '', bodyText: '' };
|
|
349
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
350
|
+
lastState = (await page.evaluate(`() => ({
|
|
351
|
+
href: location.href,
|
|
352
|
+
bodyText: document.body?.innerText || ''
|
|
353
|
+
})`)) as { href: string; bodyText: string };
|
|
354
|
+
|
|
355
|
+
if (
|
|
356
|
+
lastState.href.includes('/creator-micro/content/upload')
|
|
357
|
+
&& /继续编辑/.test(lastState.bodyText)
|
|
358
|
+
) {
|
|
359
|
+
return creationId;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
await page.wait({ time: 1 });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
throw new CommandExecutionError(
|
|
366
|
+
'未检测到抖音草稿恢复提示',
|
|
367
|
+
`当前页面: ${lastState.href || 'unknown'}`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
65
370
|
|
|
66
371
|
cli({
|
|
67
372
|
site: 'douyin',
|
|
@@ -69,6 +374,7 @@ cli({
|
|
|
69
374
|
description: '上传视频并保存为草稿',
|
|
70
375
|
domain: 'creator.douyin.com',
|
|
71
376
|
strategy: Strategy.COOKIE,
|
|
377
|
+
navigateBefore: false,
|
|
72
378
|
args: [
|
|
73
379
|
{ name: 'video', required: true, positional: true, help: '视频文件路径' },
|
|
74
380
|
{ name: 'title', required: true, help: '视频标题(≤30字)' },
|
|
@@ -76,9 +382,8 @@ cli({
|
|
|
76
382
|
{ name: 'cover', default: '', help: '封面图片路径' },
|
|
77
383
|
{ name: 'visibility', default: 'public', choices: ['public', 'friends', 'private'] },
|
|
78
384
|
],
|
|
79
|
-
columns: ['status', '
|
|
385
|
+
columns: ['status', 'draft_id'],
|
|
80
386
|
func: async (page: IPage, kwargs) => {
|
|
81
|
-
// ── Fail-fast validation ────────────────────────────────────────────
|
|
82
387
|
const videoPath = path.resolve(kwargs.video as string);
|
|
83
388
|
if (!fs.existsSync(videoPath)) {
|
|
84
389
|
throw new ArgumentError(`视频文件不存在: ${videoPath}`);
|
|
@@ -87,7 +392,6 @@ cli({
|
|
|
87
392
|
if (!['.mp4', '.mov', '.avi', '.webm'].includes(ext)) {
|
|
88
393
|
throw new ArgumentError(`不支持的视频格式: ${ext}(支持 mp4/mov/avi/webm)`);
|
|
89
394
|
}
|
|
90
|
-
const fileSize = fs.statSync(videoPath).size;
|
|
91
395
|
|
|
92
396
|
const title = kwargs.title as string;
|
|
93
397
|
if (title.length > 30) {
|
|
@@ -99,8 +403,6 @@ cli({
|
|
|
99
403
|
throw new ArgumentError('正文不能超过 1000 字');
|
|
100
404
|
}
|
|
101
405
|
|
|
102
|
-
const visibilityType = VISIBILITY_MAP[kwargs.visibility as string] ?? 0;
|
|
103
|
-
|
|
104
406
|
const coverPath = kwargs.cover as string;
|
|
105
407
|
if (coverPath) {
|
|
106
408
|
if (!fs.existsSync(path.resolve(coverPath))) {
|
|
@@ -108,174 +410,36 @@ cli({
|
|
|
108
410
|
}
|
|
109
411
|
}
|
|
110
412
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const vodJs = `fetch(${JSON.stringify(vodUrl)}, { credentials: 'include' }).then(r => r.json())`;
|
|
117
|
-
const vodRes = (await page.evaluate(vodJs)) as {
|
|
118
|
-
Result: {
|
|
119
|
-
UploadAddress: {
|
|
120
|
-
VideoId: string;
|
|
121
|
-
UploadHosts: string[];
|
|
122
|
-
StoreInfos: Array<{ Auth: string; StoreUri: string }>;
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
const { VideoId: videoId, UploadHosts, StoreInfos } = vodRes.Result.UploadAddress;
|
|
127
|
-
const tosUrl = `https://${UploadHosts[0]}/${StoreInfos[0].StoreUri}`;
|
|
128
|
-
const tosUploadInfo: TosUploadInfo = {
|
|
129
|
-
tos_upload_url: tosUrl,
|
|
130
|
-
auth: StoreInfos[0].Auth,
|
|
131
|
-
video_id: videoId,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// ── Phase 3: TOS upload ─────────────────────────────────────────────
|
|
135
|
-
await tosUpload({
|
|
136
|
-
filePath: videoPath,
|
|
137
|
-
uploadInfo: tosUploadInfo,
|
|
138
|
-
credentials,
|
|
139
|
-
onProgress: (uploaded, total) => {
|
|
140
|
-
const pct = Math.round((uploaded / total) * 100);
|
|
141
|
-
process.stderr.write(`\r 上传进度: ${pct}%`);
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
process.stderr.write('\n');
|
|
145
|
-
|
|
146
|
-
// ── Phase 4: Cover upload (optional) ────────────────────────────────
|
|
147
|
-
let coverUri = '';
|
|
148
|
-
let coverWidth = 720;
|
|
149
|
-
let coverHeight = 1280;
|
|
150
|
-
|
|
151
|
-
if (kwargs.cover) {
|
|
152
|
-
const resolvedCoverPath = path.resolve(kwargs.cover as string);
|
|
153
|
-
|
|
154
|
-
// 4A: Apply ImageX upload
|
|
155
|
-
const applyUrl = `${IMAGEX_BASE}/?Action=ApplyImageUpload&ServiceId=${IMAGEX_SERVICE_ID}&Version=2018-08-01&UploadNum=1`;
|
|
156
|
-
const applyJs = `fetch(${JSON.stringify(applyUrl)}, { credentials: 'include' }).then(r => r.json())`;
|
|
157
|
-
const applyRes = (await page.evaluate(applyJs)) as {
|
|
158
|
-
Result: {
|
|
159
|
-
UploadAddress: {
|
|
160
|
-
UploadHosts: string[];
|
|
161
|
-
StoreInfos: Array<{ Auth: string; StoreUri: string; UploadHost: string }>;
|
|
162
|
-
};
|
|
163
|
-
};
|
|
164
|
-
};
|
|
165
|
-
const { StoreInfos: imgStoreInfos } = applyRes.Result.UploadAddress;
|
|
166
|
-
const imgUploadUrl = `https://${imgStoreInfos[0].UploadHost}/${imgStoreInfos[0].StoreUri}`;
|
|
167
|
-
|
|
168
|
-
// 4B: Upload image
|
|
169
|
-
const coverStoreUri = await imagexUpload(resolvedCoverPath, {
|
|
170
|
-
upload_url: imgUploadUrl,
|
|
171
|
-
store_uri: imgStoreInfos[0].StoreUri,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// 4C: Commit ImageX upload
|
|
175
|
-
const commitUrl = `${IMAGEX_BASE}/?Action=CommitImageUpload&ServiceId=${IMAGEX_SERVICE_ID}&Version=2018-08-01`;
|
|
176
|
-
const commitBody = JSON.stringify({ SuccessObjKeys: [coverStoreUri] });
|
|
177
|
-
const commitJs = `
|
|
178
|
-
fetch(${JSON.stringify(commitUrl)}, {
|
|
179
|
-
method: 'POST',
|
|
180
|
-
credentials: 'include',
|
|
181
|
-
headers: { 'Content-Type': 'application/json' },
|
|
182
|
-
body: ${JSON.stringify(commitBody)}
|
|
183
|
-
}).then(r => r.json())
|
|
184
|
-
`;
|
|
185
|
-
await page.evaluate(commitJs);
|
|
186
|
-
|
|
187
|
-
coverUri = coverStoreUri;
|
|
413
|
+
if (!page.setFileInput) {
|
|
414
|
+
throw new CommandExecutionError(
|
|
415
|
+
'当前浏览器适配器不支持文件注入',
|
|
416
|
+
'请使用 Browser Bridge 或支持 setFileInput 的浏览器模式',
|
|
417
|
+
);
|
|
188
418
|
}
|
|
189
419
|
|
|
190
|
-
|
|
191
|
-
const enableUrl = `https://creator.douyin.com/web/api/media/video/enable/?video_id=${videoId}&aid=1128`;
|
|
192
|
-
await browserFetch(page, 'GET', enableUrl);
|
|
193
|
-
|
|
194
|
-
// ── Phase 6: Poll transcode ─────────────────────────────────────────
|
|
195
|
-
const transResult = await pollTranscode(page, videoId);
|
|
196
|
-
coverWidth = transResult.width;
|
|
197
|
-
coverHeight = transResult.height;
|
|
198
|
-
if (!coverUri) {
|
|
199
|
-
coverUri = transResult.poster_uri;
|
|
200
|
-
}
|
|
420
|
+
const visibilityLabel = VISIBILITY_LABELS[kwargs.visibility as string] ?? VISIBILITY_LABELS.public;
|
|
201
421
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
searchFrom = idx + name.length + 1;
|
|
422
|
+
await page.goto(DRAFT_UPLOAD_URL);
|
|
423
|
+
await page.wait({ selector: 'input[type="file"]', timeout: 20 });
|
|
424
|
+
await dismissKnownModals(page);
|
|
425
|
+
await page.setFileInput([videoPath], 'input[type="file"]');
|
|
426
|
+
await waitForDraftComposer(page);
|
|
427
|
+
await dismissKnownModals(page);
|
|
428
|
+
if (coverPath) {
|
|
429
|
+
const coverSelector = await prepareCustomCoverInput(page);
|
|
430
|
+
await page.setFileInput([path.resolve(coverPath)], coverSelector);
|
|
431
|
+
await waitForCoverReady(page);
|
|
213
432
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
item: {
|
|
218
|
-
common: {
|
|
219
|
-
text: caption,
|
|
220
|
-
caption: '',
|
|
221
|
-
item_title: title,
|
|
222
|
-
activity: '[]',
|
|
223
|
-
text_extra: JSON.stringify(textExtraArr),
|
|
224
|
-
challenges: '[]',
|
|
225
|
-
mentions: '[]',
|
|
226
|
-
hashtag_source: '',
|
|
227
|
-
hot_sentence: '',
|
|
228
|
-
interaction_stickers: '[]',
|
|
229
|
-
visibility_type: visibilityType,
|
|
230
|
-
download: 0,
|
|
231
|
-
is_draft: 1,
|
|
232
|
-
creation_id: generateCreationId(),
|
|
233
|
-
media_type: 4,
|
|
234
|
-
video_id: videoId,
|
|
235
|
-
music_source: 0,
|
|
236
|
-
music_id: null,
|
|
237
|
-
},
|
|
238
|
-
cover: {
|
|
239
|
-
poster: coverUri,
|
|
240
|
-
custom_cover_image_height: coverHeight,
|
|
241
|
-
custom_cover_image_width: coverWidth,
|
|
242
|
-
poster_delay: 0,
|
|
243
|
-
cover_tools_info: DEFAULT_COVER_TOOLS_INFO,
|
|
244
|
-
cover_tools_extend_info: '{}',
|
|
245
|
-
},
|
|
246
|
-
mix: {},
|
|
247
|
-
chapter: {
|
|
248
|
-
chapter: JSON.stringify({
|
|
249
|
-
chapter_abstract: '',
|
|
250
|
-
chapter_details: [],
|
|
251
|
-
chapter_type: 0,
|
|
252
|
-
}),
|
|
253
|
-
},
|
|
254
|
-
anchor: {},
|
|
255
|
-
sync: {
|
|
256
|
-
should_sync: false,
|
|
257
|
-
sync_to_toutiao: 0,
|
|
258
|
-
},
|
|
259
|
-
open_platform: {},
|
|
260
|
-
assistant: { is_preview: 0, is_post_assistant: 1 },
|
|
261
|
-
declare: { user_declare_info: '{}' },
|
|
262
|
-
},
|
|
263
|
-
};
|
|
433
|
+
await fillDraftComposer(page, { title, caption, visibilityLabel });
|
|
434
|
+
await page.wait({ time: 1 });
|
|
435
|
+
const saveResult = await clickSaveDraft(page);
|
|
264
436
|
|
|
265
|
-
const
|
|
266
|
-
const publishRes = (await browserFetch(page, 'POST', publishUrl, {
|
|
267
|
-
body: publishBody,
|
|
268
|
-
})) as { status_code: number; aweme_id: string };
|
|
269
|
-
|
|
270
|
-
const awemeId = publishRes.aweme_id;
|
|
271
|
-
if (!awemeId) {
|
|
272
|
-
throw new CommandExecutionError(`草稿保存成功但未返回 aweme_id: ${JSON.stringify(publishRes)}`);
|
|
273
|
-
}
|
|
437
|
+
const draftId = await waitForDraftResult(page, saveResult.creationId);
|
|
274
438
|
|
|
275
439
|
return [
|
|
276
440
|
{
|
|
277
|
-
status: '✅
|
|
278
|
-
|
|
441
|
+
status: '✅ 草稿已保存,可在创作中心继续编辑',
|
|
442
|
+
draft_id: draftId,
|
|
279
443
|
},
|
|
280
444
|
];
|
|
281
445
|
},
|