@jackwener/opencli 1.5.6 → 1.5.7
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 +26 -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/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 +794 -513
- package/extension/src/background.test.ts +202 -2
- package/extension/src/background.ts +174 -10
- package/extension/src/cdp.ts +12 -0
- 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/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
|
@@ -1,6 +1,86 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { wrapForEval } from '../../browser/utils.js';
|
|
2
6
|
import { getRegistry } from '../../registry.js';
|
|
3
|
-
import './draft.js';
|
|
7
|
+
import { buildCoverCheckPanelTextJs } from './draft.js';
|
|
8
|
+
// ─── Shared test helpers ────────────────────────────────────────────
|
|
9
|
+
const tempDirs = [];
|
|
10
|
+
function createTempVideo(name = 'demo.mp4') {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-douyin-draft-'));
|
|
12
|
+
tempDirs.push(dir);
|
|
13
|
+
const filePath = path.join(dir, name);
|
|
14
|
+
fs.writeFileSync(filePath, Buffer.from([0, 0, 0, 20, 102, 116, 121, 112]));
|
|
15
|
+
return filePath;
|
|
16
|
+
}
|
|
17
|
+
function createTempCover(videoPath, name = 'cover.jpg') {
|
|
18
|
+
const filePath = path.join(path.dirname(videoPath), name);
|
|
19
|
+
fs.writeFileSync(filePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
20
|
+
return filePath;
|
|
21
|
+
}
|
|
22
|
+
function getDraftCommand() {
|
|
23
|
+
const registry = getRegistry();
|
|
24
|
+
const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'draft');
|
|
25
|
+
if (!cmd?.func)
|
|
26
|
+
throw new Error('douyin draft command not registered');
|
|
27
|
+
return cmd;
|
|
28
|
+
}
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
for (const dir of tempDirs) {
|
|
31
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
function createFakeTree(text, children = []) {
|
|
35
|
+
const node = {
|
|
36
|
+
textContent: text,
|
|
37
|
+
parentElement: null,
|
|
38
|
+
querySelectorAll: () => [],
|
|
39
|
+
};
|
|
40
|
+
node.querySelectorAll = () => {
|
|
41
|
+
const descendants = [];
|
|
42
|
+
for (const child of children) {
|
|
43
|
+
descendants.push(child, ...child.querySelectorAll('*'));
|
|
44
|
+
}
|
|
45
|
+
return descendants;
|
|
46
|
+
};
|
|
47
|
+
for (const child of children) {
|
|
48
|
+
child.parentElement = node;
|
|
49
|
+
}
|
|
50
|
+
return node;
|
|
51
|
+
}
|
|
52
|
+
function createPageMock(evaluateResults, overrides = {}) {
|
|
53
|
+
const evaluate = vi.fn();
|
|
54
|
+
for (const result of evaluateResults) {
|
|
55
|
+
evaluate.mockResolvedValueOnce(result);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
59
|
+
evaluate,
|
|
60
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
61
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
62
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
63
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
64
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
65
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
66
|
+
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
67
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
68
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
69
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
70
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
71
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
72
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
73
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
74
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
75
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
76
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
77
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
78
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
79
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
80
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
81
|
+
...overrides,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
4
84
|
describe('douyin draft registration', () => {
|
|
5
85
|
it('registers the draft command', () => {
|
|
6
86
|
const registry = getRegistry();
|
|
@@ -8,4 +88,279 @@ describe('douyin draft registration', () => {
|
|
|
8
88
|
const cmd = values.find(c => c.site === 'douyin' && c.name === 'draft');
|
|
9
89
|
expect(cmd).toBeDefined();
|
|
10
90
|
});
|
|
91
|
+
it('extracts the higher quick-check panel instead of stopping at a header-only ancestor', () => {
|
|
92
|
+
const marker = createFakeTree('快速检测');
|
|
93
|
+
const state = createFakeTree('重新检测');
|
|
94
|
+
const header = createFakeTree('快速检测', [marker]);
|
|
95
|
+
const status = createFakeTree('重新检测', [state]);
|
|
96
|
+
const panel = createFakeTree('快速检测重新检测', [header, status]);
|
|
97
|
+
const body = createFakeTree('body', [panel]);
|
|
98
|
+
const g = globalThis;
|
|
99
|
+
const originalDocument = g.document;
|
|
100
|
+
g.document = {
|
|
101
|
+
body,
|
|
102
|
+
querySelectorAll: () => [marker, state],
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
expect(eval(buildCoverCheckPanelTextJs())()).toBe('快速检测重新检测');
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
g.document = originalDocument;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
it('returns empty when only header text exists and no exact quick-check state node is present', () => {
|
|
112
|
+
const marker = createFakeTree('快速检测');
|
|
113
|
+
const note = createFakeTree('检测说明');
|
|
114
|
+
const header = createFakeTree('快速检测检测说明', [marker, note]);
|
|
115
|
+
const body = createFakeTree('body', [header]);
|
|
116
|
+
const g = globalThis;
|
|
117
|
+
const originalDocument = g.document;
|
|
118
|
+
g.document = {
|
|
119
|
+
body,
|
|
120
|
+
querySelectorAll: () => [marker, note],
|
|
121
|
+
};
|
|
122
|
+
try {
|
|
123
|
+
expect(eval(buildCoverCheckPanelTextJs())()).toBe('');
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
g.document = originalDocument;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
it('extracts the quick-check panel when busy state is rendered as a single 封面检测中 node', () => {
|
|
130
|
+
const marker = createFakeTree('快速检测');
|
|
131
|
+
const busy = createFakeTree('封面检测中');
|
|
132
|
+
const header = createFakeTree('快速检测', [marker]);
|
|
133
|
+
const status = createFakeTree('封面检测中', [busy]);
|
|
134
|
+
const panel = createFakeTree('快速检测封面检测中', [header, status]);
|
|
135
|
+
const body = createFakeTree('body', [panel]);
|
|
136
|
+
const g = globalThis;
|
|
137
|
+
const originalDocument = g.document;
|
|
138
|
+
g.document = {
|
|
139
|
+
body,
|
|
140
|
+
querySelectorAll: () => [marker, busy],
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
expect(eval(buildCoverCheckPanelTextJs())()).toBe('快速检测封面检测中');
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
g.document = originalDocument;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
it('uploads through the official creator draft page and saves the draft session', async () => {
|
|
150
|
+
const cmd = getDraftCommand();
|
|
151
|
+
const videoPath = createTempVideo('demo.mp4');
|
|
152
|
+
const page = createPageMock([
|
|
153
|
+
undefined,
|
|
154
|
+
{ href: 'https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page', ready: true, bodyText: '' },
|
|
155
|
+
undefined,
|
|
156
|
+
true,
|
|
157
|
+
true,
|
|
158
|
+
true,
|
|
159
|
+
{ ok: true, text: '暂存离开', creationId: 'creation-001' },
|
|
160
|
+
{
|
|
161
|
+
href: 'https://creator.douyin.com/creator-micro/content/upload?enter_from=publish',
|
|
162
|
+
bodyText: '你还有上次未发布的视频,是否继续编辑?继续编辑放弃',
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
const rows = await cmd.func(page, {
|
|
166
|
+
video: videoPath,
|
|
167
|
+
title: '最小修复验证',
|
|
168
|
+
caption: 'opencli draft e2e',
|
|
169
|
+
cover: '',
|
|
170
|
+
visibility: 'friends',
|
|
171
|
+
});
|
|
172
|
+
expect(page.goto).toHaveBeenCalledWith('https://creator.douyin.com/creator-micro/content/upload');
|
|
173
|
+
expect(page.wait).toHaveBeenCalledWith({
|
|
174
|
+
selector: 'input[type="file"]',
|
|
175
|
+
timeout: 20,
|
|
176
|
+
});
|
|
177
|
+
expect(page.setFileInput).toHaveBeenCalledWith([videoPath], 'input[type="file"]');
|
|
178
|
+
const evaluateCalls = page.evaluate.mock.calls.map((args) => String(args[0]));
|
|
179
|
+
expect(evaluateCalls.some((code) => code.includes('填写作品标题'))).toBe(true);
|
|
180
|
+
expect(evaluateCalls.some((code) => code.includes('好友可见'))).toBe(true);
|
|
181
|
+
expect(evaluateCalls.some((code) => code.includes('暂存离开'))).toBe(true);
|
|
182
|
+
expect(rows).toEqual([
|
|
183
|
+
{
|
|
184
|
+
status: '✅ 草稿已保存,可在创作中心继续编辑',
|
|
185
|
+
draft_id: 'creation-001',
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
it('waits for the composer when upload processing is slower than the first few polls', async () => {
|
|
190
|
+
const cmd = getDraftCommand();
|
|
191
|
+
const videoPath = createTempVideo('slow.mp4');
|
|
192
|
+
const page = createPageMock([
|
|
193
|
+
undefined,
|
|
194
|
+
{ href: 'https://creator.douyin.com/creator-micro/content/upload', ready: false, bodyText: '上传中 42%' },
|
|
195
|
+
{ href: 'https://creator.douyin.com/creator-micro/content/upload', ready: false, bodyText: '转码中' },
|
|
196
|
+
{ href: 'https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page', ready: true, bodyText: '' },
|
|
197
|
+
undefined,
|
|
198
|
+
true,
|
|
199
|
+
true,
|
|
200
|
+
{ ok: true, text: '暂存离开', creationId: 'creation-slow' },
|
|
201
|
+
{
|
|
202
|
+
href: 'https://creator.douyin.com/creator-micro/content/upload?enter_from=publish',
|
|
203
|
+
bodyText: '你还有上次未发布的视频,是否继续编辑?继续编辑放弃',
|
|
204
|
+
},
|
|
205
|
+
]);
|
|
206
|
+
const rows = await cmd.func(page, {
|
|
207
|
+
video: videoPath,
|
|
208
|
+
title: '慢上传验证',
|
|
209
|
+
caption: '',
|
|
210
|
+
cover: '',
|
|
211
|
+
visibility: 'public',
|
|
212
|
+
});
|
|
213
|
+
expect(rows).toEqual([
|
|
214
|
+
{
|
|
215
|
+
status: '✅ 草稿已保存,可在创作中心继续编辑',
|
|
216
|
+
draft_id: 'creation-slow',
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
expect(page.wait).toHaveBeenCalledWith({ time: 0.5 });
|
|
220
|
+
const shortWaitCalls = page.wait.mock.calls.filter(([arg]) => JSON.stringify(arg) === JSON.stringify({ time: 0.5 }));
|
|
221
|
+
expect(shortWaitCalls).toHaveLength(2);
|
|
222
|
+
});
|
|
223
|
+
it('fails fast when the save action does not expose a draft creation id', async () => {
|
|
224
|
+
const cmd = getDraftCommand();
|
|
225
|
+
const videoPath = createTempVideo('missing-id.mp4');
|
|
226
|
+
const page = createPageMock([
|
|
227
|
+
undefined,
|
|
228
|
+
{ href: 'https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page', ready: true, bodyText: '' },
|
|
229
|
+
undefined,
|
|
230
|
+
true,
|
|
231
|
+
true,
|
|
232
|
+
{ ok: true, text: '暂存离开', creationId: '' },
|
|
233
|
+
]);
|
|
234
|
+
await expect(cmd.func(page, {
|
|
235
|
+
video: videoPath,
|
|
236
|
+
title: '缺失 creation id',
|
|
237
|
+
caption: '',
|
|
238
|
+
cover: '',
|
|
239
|
+
visibility: 'public',
|
|
240
|
+
})).rejects.toThrow('点击草稿按钮失败: creation-id-missing');
|
|
241
|
+
});
|
|
242
|
+
it('uses the dedicated cover upload input when a custom cover is provided', async () => {
|
|
243
|
+
const cmd = getDraftCommand();
|
|
244
|
+
const videoPath = createTempVideo('demo.mp4');
|
|
245
|
+
const coverPath = createTempCover(videoPath);
|
|
246
|
+
const page = createPageMock([
|
|
247
|
+
undefined,
|
|
248
|
+
{ href: 'https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page', ready: true, bodyText: '' },
|
|
249
|
+
undefined,
|
|
250
|
+
1,
|
|
251
|
+
{ ok: false, reason: 'cover-input-pending' },
|
|
252
|
+
{ ok: true, selector: '[data-opencli-cover-input="1"]' },
|
|
253
|
+
'快速检测检测中',
|
|
254
|
+
'快速检测重新检测',
|
|
255
|
+
true,
|
|
256
|
+
true,
|
|
257
|
+
{ ok: true, text: '暂存离开', creationId: 'creation-002' },
|
|
258
|
+
{
|
|
259
|
+
href: 'https://creator.douyin.com/creator-micro/content/upload?enter_from=publish',
|
|
260
|
+
bodyText: '你还有上次未发布的视频,是否继续编辑?继续编辑放弃',
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
const rows = await cmd.func(page, {
|
|
264
|
+
video: videoPath,
|
|
265
|
+
title: '封面上传验证',
|
|
266
|
+
caption: '',
|
|
267
|
+
cover: coverPath,
|
|
268
|
+
visibility: 'public',
|
|
269
|
+
});
|
|
270
|
+
expect(page.setFileInput).toHaveBeenNthCalledWith(1, [videoPath], 'input[type="file"]');
|
|
271
|
+
expect(page.setFileInput).toHaveBeenNthCalledWith(2, [coverPath], '[data-opencli-cover-input="1"]');
|
|
272
|
+
const shortWaitCalls = page.wait.mock.calls.filter(([arg]) => JSON.stringify(arg) === JSON.stringify({ time: 0.5 }));
|
|
273
|
+
expect(shortWaitCalls).toHaveLength(2);
|
|
274
|
+
const evaluateCalls = page.evaluate.mock.calls.map((args) => String(args[0]));
|
|
275
|
+
expect(evaluateCalls.some((code) => code.includes('上传新封面'))).toBe(true);
|
|
276
|
+
expect(evaluateCalls.some((code) => code.includes("text.includes('快速检测检测')"))).toBe(false);
|
|
277
|
+
expect(() => {
|
|
278
|
+
for (const code of evaluateCalls) {
|
|
279
|
+
new Function(wrapForEval(code));
|
|
280
|
+
}
|
|
281
|
+
}).not.toThrow();
|
|
282
|
+
expect(rows).toEqual([
|
|
283
|
+
{
|
|
284
|
+
status: '✅ 草稿已保存,可在创作中心继续编辑',
|
|
285
|
+
draft_id: 'creation-002',
|
|
286
|
+
},
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
it('waits for a late cover-section update before treating the custom cover as ready', async () => {
|
|
290
|
+
const cmd = getDraftCommand();
|
|
291
|
+
const videoPath = createTempVideo('cover-race.mp4');
|
|
292
|
+
const coverPath = createTempCover(videoPath, 'cover-race.jpg');
|
|
293
|
+
const page = createPageMock([
|
|
294
|
+
undefined,
|
|
295
|
+
{ href: 'https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page', ready: true, bodyText: '' },
|
|
296
|
+
undefined,
|
|
297
|
+
1,
|
|
298
|
+
{ ok: true, selector: '[data-opencli-cover-input="1"]' },
|
|
299
|
+
'快速检测重新检测',
|
|
300
|
+
'快速检测重新检测',
|
|
301
|
+
'快速检测重新检测',
|
|
302
|
+
'快速检测检测中',
|
|
303
|
+
'快速检测横/竖双封面缺失',
|
|
304
|
+
true,
|
|
305
|
+
true,
|
|
306
|
+
{ ok: true, text: '暂存离开', creationId: 'creation-cover-race' },
|
|
307
|
+
{
|
|
308
|
+
href: 'https://creator.douyin.com/creator-micro/content/upload?enter_from=publish',
|
|
309
|
+
bodyText: '你还有上次未发布的视频,是否继续编辑?继续编辑放弃',
|
|
310
|
+
},
|
|
311
|
+
]);
|
|
312
|
+
const rows = await cmd.func(page, {
|
|
313
|
+
video: videoPath,
|
|
314
|
+
title: '封面竞态验证',
|
|
315
|
+
caption: '',
|
|
316
|
+
cover: coverPath,
|
|
317
|
+
visibility: 'public',
|
|
318
|
+
});
|
|
319
|
+
expect(rows).toEqual([
|
|
320
|
+
{
|
|
321
|
+
status: '✅ 草稿已保存,可在创作中心继续编辑',
|
|
322
|
+
draft_id: 'creation-cover-race',
|
|
323
|
+
},
|
|
324
|
+
]);
|
|
325
|
+
const shortWaitCalls = page.wait.mock.calls.filter(([arg]) => JSON.stringify(arg) === JSON.stringify({ time: 0.5 }));
|
|
326
|
+
expect(shortWaitCalls).toHaveLength(4);
|
|
327
|
+
});
|
|
328
|
+
it('accepts the same ready label after cover busy state when the quick-check panel actually transitioned', async () => {
|
|
329
|
+
const cmd = getDraftCommand();
|
|
330
|
+
const videoPath = createTempVideo('cover-same-ready.mp4');
|
|
331
|
+
const coverPath = createTempCover(videoPath, 'cover-same-ready.jpg');
|
|
332
|
+
const page = createPageMock([
|
|
333
|
+
undefined,
|
|
334
|
+
{ href: 'https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page', ready: true, bodyText: '' },
|
|
335
|
+
undefined,
|
|
336
|
+
1,
|
|
337
|
+
{ ok: true, selector: '[data-opencli-cover-input="1"]' },
|
|
338
|
+
'快速检测重新检测',
|
|
339
|
+
'快速检测重新检测',
|
|
340
|
+
'快速检测检测中',
|
|
341
|
+
'快速检测重新检测',
|
|
342
|
+
true,
|
|
343
|
+
true,
|
|
344
|
+
{ ok: true, text: '暂存离开', creationId: 'creation-cover-same-ready' },
|
|
345
|
+
{
|
|
346
|
+
href: 'https://creator.douyin.com/creator-micro/content/upload?enter_from=publish',
|
|
347
|
+
bodyText: '你还有上次未发布的视频,是否继续编辑?继续编辑放弃',
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
350
|
+
const rows = await cmd.func(page, {
|
|
351
|
+
video: videoPath,
|
|
352
|
+
title: '封面同文案验证',
|
|
353
|
+
caption: '',
|
|
354
|
+
cover: coverPath,
|
|
355
|
+
visibility: 'public',
|
|
356
|
+
});
|
|
357
|
+
expect(rows).toEqual([
|
|
358
|
+
{
|
|
359
|
+
status: '✅ 草稿已保存,可在创作中心继续编辑',
|
|
360
|
+
draft_id: 'creation-cover-same-ready',
|
|
361
|
+
},
|
|
362
|
+
]);
|
|
363
|
+
const shortWaitCalls = page.wait.mock.calls.filter(([arg]) => JSON.stringify(arg) === JSON.stringify({ time: 0.5 }));
|
|
364
|
+
expect(shortWaitCalls).toHaveLength(3);
|
|
365
|
+
});
|
|
11
366
|
});
|
|
@@ -34,9 +34,16 @@ cli({
|
|
|
34
34
|
const kw = kwargs.keyword;
|
|
35
35
|
const url = `https://creator.douyin.com/aweme/v1/hotspot/recommend/?${kw ? `keyword=${encodeURIComponent(kw)}&` : ''}aid=1128`;
|
|
36
36
|
const res = await browserFetch(page, 'GET', url);
|
|
37
|
-
|
|
37
|
+
const items = res.hotspot_list
|
|
38
|
+
?? res.all_sentences?.map(h => ({
|
|
39
|
+
sentence: h.word ?? '',
|
|
40
|
+
hot_value: h.hot_value,
|
|
41
|
+
sentence_id: h.sentence_id ?? '',
|
|
42
|
+
}))
|
|
43
|
+
?? [];
|
|
44
|
+
return items.slice(0, kwargs.limit).map(h => ({
|
|
38
45
|
name: h.sentence,
|
|
39
|
-
id: '',
|
|
46
|
+
id: 'sentence_id' in h ? h.sentence_id : '',
|
|
40
47
|
view_count: h.hot_value,
|
|
41
48
|
}));
|
|
42
49
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { browserFetchMock } = vi.hoisted(() => ({
|
|
3
|
+
browserFetchMock: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('./_shared/browser-fetch.js', () => ({
|
|
6
|
+
browserFetch: browserFetchMock,
|
|
7
|
+
}));
|
|
2
8
|
import { getRegistry } from '../../registry.js';
|
|
3
9
|
import './hashtag.js';
|
|
4
|
-
describe('douyin hashtag
|
|
10
|
+
describe('douyin hashtag', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
browserFetchMock.mockReset();
|
|
13
|
+
});
|
|
5
14
|
it('registers the hashtag command', () => {
|
|
6
15
|
const registry = getRegistry();
|
|
7
16
|
const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'hashtag');
|
|
@@ -22,4 +31,28 @@ describe('douyin hashtag registration', () => {
|
|
|
22
31
|
const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'hashtag');
|
|
23
32
|
expect(cmd?.strategy).toBe('cookie');
|
|
24
33
|
});
|
|
34
|
+
it('parses the current hotspot recommendation shape', async () => {
|
|
35
|
+
const registry = getRegistry();
|
|
36
|
+
const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'hashtag');
|
|
37
|
+
expect(command?.func).toBeDefined();
|
|
38
|
+
if (!command?.func)
|
|
39
|
+
throw new Error('douyin hashtag command not registered');
|
|
40
|
+
browserFetchMock.mockResolvedValueOnce({
|
|
41
|
+
all_sentences: [
|
|
42
|
+
{
|
|
43
|
+
word: '在公园花海里大晒一场',
|
|
44
|
+
hot_value: 12141172,
|
|
45
|
+
sentence_id: '2448416',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
const rows = await command.func({}, { action: 'hot', keyword: '', limit: 5 });
|
|
50
|
+
expect(rows).toEqual([
|
|
51
|
+
{
|
|
52
|
+
name: '在公园花海里大晒一场',
|
|
53
|
+
id: '2448416',
|
|
54
|
+
view_count: 12141172,
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
25
58
|
});
|
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
func: async (page, _kwargs) => {
|
|
13
13
|
const url = 'https://creator.douyin.com/web/api/media/user/info/?aid=1128';
|
|
14
14
|
const res = (await browserFetch(page, 'GET', url));
|
|
15
|
-
const u = res.user_info;
|
|
15
|
+
const u = res.user_info ?? res.user;
|
|
16
16
|
if (!u)
|
|
17
17
|
throw new CommandExecutionError('用户信息获取失败,请确认已登录 creator.douyin.com');
|
|
18
18
|
return [
|
|
@@ -1,11 +1,46 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { browserFetchMock } = vi.hoisted(() => ({
|
|
3
|
+
browserFetchMock: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('./_shared/browser-fetch.js', () => ({
|
|
6
|
+
browserFetch: browserFetchMock,
|
|
7
|
+
}));
|
|
2
8
|
import { getRegistry } from '../../registry.js';
|
|
3
9
|
import './profile.js';
|
|
4
10
|
describe('douyin profile registration', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
browserFetchMock.mockReset();
|
|
13
|
+
});
|
|
5
14
|
it('registers the profile command', () => {
|
|
6
15
|
const registry = getRegistry();
|
|
7
16
|
const values = [...registry.values()];
|
|
8
17
|
const cmd = values.find(c => c.site === 'douyin' && c.name === 'profile');
|
|
9
18
|
expect(cmd).toBeDefined();
|
|
10
19
|
});
|
|
20
|
+
it('maps the current user payload shape returned by creator center', async () => {
|
|
21
|
+
const registry = getRegistry();
|
|
22
|
+
const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'profile');
|
|
23
|
+
expect(cmd?.func).toBeDefined();
|
|
24
|
+
if (!cmd?.func)
|
|
25
|
+
throw new Error('douyin profile command not registered');
|
|
26
|
+
browserFetchMock.mockResolvedValueOnce({
|
|
27
|
+
user: {
|
|
28
|
+
uid: '100',
|
|
29
|
+
nickname: 'creator',
|
|
30
|
+
follower_count: 12,
|
|
31
|
+
following_count: 3,
|
|
32
|
+
aweme_count: 7,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const rows = await cmd.func({}, {});
|
|
36
|
+
expect(rows).toEqual([
|
|
37
|
+
{
|
|
38
|
+
uid: '100',
|
|
39
|
+
nickname: 'creator',
|
|
40
|
+
follower_count: 12,
|
|
41
|
+
following_count: 3,
|
|
42
|
+
aweme_count: 7,
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
11
46
|
});
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { browserFetch } from './_shared/browser-fetch.js';
|
|
3
|
+
function normalizeVideoStatus(status, publicTime) {
|
|
4
|
+
if (typeof status === 'number')
|
|
5
|
+
return status;
|
|
6
|
+
if (!status)
|
|
7
|
+
return publicTime && publicTime > Date.now() / 1000 ? 'scheduled' : 'published';
|
|
8
|
+
if (status.is_delete)
|
|
9
|
+
return 'deleted';
|
|
10
|
+
if (status.is_prohibited)
|
|
11
|
+
return 'prohibited';
|
|
12
|
+
if (status.in_reviewing)
|
|
13
|
+
return 'reviewing';
|
|
14
|
+
if (status.is_private)
|
|
15
|
+
return 'private';
|
|
16
|
+
if (publicTime && publicTime > Date.now() / 1000)
|
|
17
|
+
return 'scheduled';
|
|
18
|
+
return 'published';
|
|
19
|
+
}
|
|
3
20
|
cli({
|
|
4
21
|
site: 'douyin',
|
|
5
22
|
name: 'videos',
|
|
@@ -17,18 +34,18 @@ cli({
|
|
|
17
34
|
const statusNum = statusMap[kwargs.status] ?? 0;
|
|
18
35
|
const url = `https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=${kwargs.limit}&page_num=${kwargs.page}&status=${statusNum}`;
|
|
19
36
|
const res = (await browserFetch(page, 'GET', url));
|
|
20
|
-
let items = res.data?.work_list ?? [];
|
|
37
|
+
let items = res.data?.work_list ?? res.aweme_list ?? [];
|
|
21
38
|
// The API has a bug with status=16 for scheduled, so filter client-side
|
|
22
39
|
if (kwargs.status === 'scheduled') {
|
|
23
|
-
items = items.filter((v) => v.public_time > Date.now() / 1000);
|
|
40
|
+
items = items.filter((v) => (v.public_time ?? 0) > Date.now() / 1000);
|
|
24
41
|
}
|
|
25
42
|
return items.map((v) => ({
|
|
26
43
|
aweme_id: v.aweme_id,
|
|
27
|
-
title: v.desc,
|
|
28
|
-
status: v.status,
|
|
44
|
+
title: v.desc ?? '',
|
|
45
|
+
status: normalizeVideoStatus(v.status, v.public_time),
|
|
29
46
|
play_count: v.statistics?.play_count ?? 0,
|
|
30
47
|
digg_count: v.statistics?.digg_count ?? 0,
|
|
31
|
-
create_time: new Date(v.create_time * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
|
|
48
|
+
create_time: new Date((v.create_time ?? v.public_time ?? 0) * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
|
|
32
49
|
}));
|
|
33
50
|
},
|
|
34
51
|
});
|
|
@@ -1,11 +1,54 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { browserFetchMock } = vi.hoisted(() => ({
|
|
3
|
+
browserFetchMock: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('./_shared/browser-fetch.js', () => ({
|
|
6
|
+
browserFetch: browserFetchMock,
|
|
7
|
+
}));
|
|
2
8
|
import { getRegistry } from '../../registry.js';
|
|
3
9
|
import './videos.js';
|
|
4
|
-
describe('douyin videos
|
|
10
|
+
describe('douyin videos', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
browserFetchMock.mockReset();
|
|
13
|
+
});
|
|
5
14
|
it('registers the videos command', () => {
|
|
6
15
|
const registry = getRegistry();
|
|
7
16
|
const values = [...registry.values()];
|
|
8
17
|
const cmd = values.find(c => c.site === 'douyin' && c.name === 'videos');
|
|
9
18
|
expect(cmd).toBeDefined();
|
|
10
19
|
});
|
|
20
|
+
it('parses the current creator work_list api shape', async () => {
|
|
21
|
+
const registry = getRegistry();
|
|
22
|
+
const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'videos');
|
|
23
|
+
expect(command?.func).toBeDefined();
|
|
24
|
+
if (!command?.func)
|
|
25
|
+
throw new Error('douyin videos command not registered');
|
|
26
|
+
browserFetchMock.mockResolvedValueOnce({
|
|
27
|
+
aweme_list: [
|
|
28
|
+
{
|
|
29
|
+
aweme_id: '7000000000000000001',
|
|
30
|
+
desc: '测试视频标题',
|
|
31
|
+
create_time: 1581571130,
|
|
32
|
+
statistics: {
|
|
33
|
+
play_count: 0,
|
|
34
|
+
digg_count: 12,
|
|
35
|
+
},
|
|
36
|
+
status: {
|
|
37
|
+
is_private: true,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
const rows = await command.func({}, { limit: 5, page: 1, status: 'all' });
|
|
43
|
+
expect(rows).toEqual([
|
|
44
|
+
{
|
|
45
|
+
aweme_id: '7000000000000000001',
|
|
46
|
+
title: '测试视频标题',
|
|
47
|
+
status: 'private',
|
|
48
|
+
play_count: 0,
|
|
49
|
+
digg_count: 12,
|
|
50
|
+
create_time: new Date(1581571130 * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
11
54
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for issue #625.
|
|
3
|
+
* Facebook search must navigate in the pipeline before DOM extraction.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import yaml from 'js-yaml';
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { executePipeline } from '../../pipeline/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Minimal browser mock for pipeline execution tests.
|
|
13
|
+
* Only methods touched by this adapter path are implemented.
|
|
14
|
+
*/
|
|
15
|
+
function createMockPage() {
|
|
16
|
+
return {
|
|
17
|
+
goto: vi.fn(),
|
|
18
|
+
evaluate: vi.fn().mockResolvedValue([]),
|
|
19
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
20
|
+
snapshot: vi.fn().mockResolvedValue(''),
|
|
21
|
+
click: vi.fn(),
|
|
22
|
+
typeText: vi.fn(),
|
|
23
|
+
pressKey: vi.fn(),
|
|
24
|
+
scrollTo: vi.fn(),
|
|
25
|
+
getFormState: vi.fn().mockResolvedValue({}),
|
|
26
|
+
wait: vi.fn(),
|
|
27
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
28
|
+
closeTab: vi.fn(),
|
|
29
|
+
newTab: vi.fn(),
|
|
30
|
+
selectTab: vi.fn(),
|
|
31
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
32
|
+
consoleMessages: vi.fn().mockResolvedValue(''),
|
|
33
|
+
scroll: vi.fn(),
|
|
34
|
+
autoScroll: vi.fn(),
|
|
35
|
+
installInterceptor: vi.fn(),
|
|
36
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
37
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
38
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
describe('facebook search pipeline', () => {
|
|
42
|
+
it('navigates to search results before extracting DOM data', async () => {
|
|
43
|
+
// Load the YAML adapter directly so the regression test covers the shipped command definition.
|
|
44
|
+
const filePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'search.yaml');
|
|
45
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
46
|
+
const command = yaml.load(raw);
|
|
47
|
+
const pipeline = command.pipeline ?? [];
|
|
48
|
+
const page = createMockPage();
|
|
49
|
+
await executePipeline(page, pipeline, {
|
|
50
|
+
args: { query: 'AI agent', limit: 3 },
|
|
51
|
+
});
|
|
52
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://www.facebook.com');
|
|
53
|
+
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://www.facebook.com/search/top?q=AI%20agent', {
|
|
54
|
+
waitUntil: undefined,
|
|
55
|
+
settleMs: 4000,
|
|
56
|
+
});
|
|
57
|
+
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(String(page.evaluate.mock.calls[0]?.[0] ?? '')).not.toContain('window.location.href');
|
|
59
|
+
});
|
|
60
|
+
});
|