@jackwener/opencli 0.9.5 → 0.9.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/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
- package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
- package/.github/dependabot.yml +27 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/workflows/ci.yml +14 -8
- package/.github/workflows/e2e-headed.yml +6 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release-please.yml +25 -0
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/security.yml +36 -0
- package/CLI-ELECTRON.md +89 -36
- package/CONTRIBUTING.md +167 -0
- package/README.md +98 -32
- package/README.zh-CN.md +99 -33
- package/dist/browser/discover.js +22 -7
- package/dist/browser.test.js +23 -0
- package/dist/build-manifest.d.ts +26 -0
- package/dist/build-manifest.js +132 -60
- package/dist/build-manifest.test.d.ts +1 -0
- package/dist/build-manifest.test.js +26 -0
- package/dist/cli-manifest.json +1875 -271
- package/dist/clis/antigravity/model.js +2 -2
- package/dist/clis/antigravity/send.js +2 -2
- package/dist/clis/bilibili/download.d.ts +10 -0
- package/dist/clis/bilibili/download.js +135 -0
- package/dist/clis/chatgpt/ask.d.ts +1 -0
- package/dist/clis/chatgpt/ask.js +68 -0
- package/dist/clis/chatgpt/send.js +11 -0
- package/dist/clis/chatwise/ask.d.ts +1 -0
- package/dist/clis/chatwise/ask.js +76 -0
- package/dist/clis/chatwise/export.d.ts +1 -0
- package/dist/clis/chatwise/export.js +46 -0
- package/dist/clis/chatwise/history.d.ts +1 -0
- package/dist/clis/chatwise/history.js +43 -0
- package/dist/clis/chatwise/model.d.ts +1 -0
- package/dist/clis/chatwise/model.js +81 -0
- package/dist/clis/chatwise/new.d.ts +1 -0
- package/dist/clis/chatwise/new.js +18 -0
- package/dist/clis/chatwise/read.d.ts +1 -0
- package/dist/clis/chatwise/read.js +39 -0
- package/dist/clis/chatwise/screenshot.d.ts +1 -0
- package/dist/clis/chatwise/screenshot.js +27 -0
- package/dist/clis/chatwise/send.d.ts +1 -0
- package/dist/clis/chatwise/send.js +45 -0
- package/dist/clis/chatwise/status.d.ts +1 -0
- package/dist/clis/chatwise/status.js +22 -0
- package/dist/clis/codex/ask.d.ts +1 -0
- package/dist/clis/codex/ask.js +67 -0
- package/dist/clis/codex/export.d.ts +1 -0
- package/dist/clis/codex/export.js +37 -0
- package/dist/clis/codex/history.d.ts +1 -0
- package/dist/clis/codex/history.js +43 -0
- package/dist/clis/codex/read.js +3 -5
- package/dist/clis/codex/screenshot.d.ts +1 -0
- package/dist/clis/codex/screenshot.js +27 -0
- package/dist/clis/codex/send.js +3 -6
- package/dist/clis/codex/status.js +2 -1
- package/dist/clis/cursor/ask.d.ts +1 -0
- package/dist/clis/cursor/ask.js +69 -0
- package/dist/clis/cursor/composer.js +9 -28
- package/dist/clis/cursor/export.d.ts +1 -0
- package/dist/clis/cursor/export.js +51 -0
- package/dist/clis/cursor/history.d.ts +1 -0
- package/dist/clis/cursor/history.js +43 -0
- package/dist/clis/cursor/new.js +4 -13
- package/dist/clis/cursor/screenshot.d.ts +1 -0
- package/dist/clis/cursor/screenshot.js +31 -0
- package/dist/clis/discord-app/channels.d.ts +1 -0
- package/dist/clis/discord-app/channels.js +45 -0
- package/dist/clis/discord-app/members.d.ts +1 -0
- package/dist/clis/discord-app/members.js +38 -0
- package/dist/clis/discord-app/read.d.ts +1 -0
- package/dist/clis/discord-app/read.js +45 -0
- package/dist/clis/discord-app/search.d.ts +1 -0
- package/dist/clis/discord-app/search.js +56 -0
- package/dist/clis/discord-app/send.d.ts +1 -0
- package/dist/clis/discord-app/send.js +27 -0
- package/dist/clis/discord-app/servers.d.ts +1 -0
- package/dist/clis/discord-app/servers.js +36 -0
- package/dist/clis/discord-app/status.d.ts +1 -0
- package/dist/clis/discord-app/status.js +16 -0
- package/dist/clis/feishu/new.d.ts +1 -0
- package/dist/clis/feishu/new.js +27 -0
- package/dist/clis/feishu/read.d.ts +1 -0
- package/dist/clis/feishu/read.js +40 -0
- package/dist/clis/feishu/search.d.ts +1 -0
- package/dist/clis/feishu/search.js +30 -0
- package/dist/clis/feishu/send.d.ts +1 -0
- package/dist/clis/feishu/send.js +39 -0
- package/dist/clis/feishu/status.d.ts +1 -0
- package/dist/clis/feishu/status.js +28 -0
- package/dist/clis/grok/ask.d.ts +1 -0
- package/dist/clis/grok/ask.js +82 -0
- package/dist/clis/grok/debug.d.ts +1 -0
- package/dist/clis/grok/debug.js +45 -0
- package/dist/clis/jimeng/generate.yaml +84 -0
- package/dist/clis/jimeng/history.yaml +47 -0
- package/dist/clis/linux-do/categories.yaml +41 -0
- package/dist/clis/linux-do/category.yaml +49 -0
- package/dist/clis/linux-do/hot.yaml +50 -0
- package/dist/clis/linux-do/latest.yaml +40 -0
- package/dist/clis/linux-do/search.yaml +45 -0
- package/dist/clis/linux-do/topic.yaml +38 -0
- package/dist/clis/notion/export.d.ts +1 -0
- package/dist/clis/notion/export.js +31 -0
- package/dist/clis/notion/favorites.d.ts +1 -0
- package/dist/clis/notion/favorites.js +84 -0
- package/dist/clis/notion/new.d.ts +1 -0
- package/dist/clis/notion/new.js +34 -0
- package/dist/clis/notion/read.d.ts +1 -0
- package/dist/clis/notion/read.js +30 -0
- package/dist/clis/notion/search.d.ts +1 -0
- package/dist/clis/notion/search.js +46 -0
- package/dist/clis/notion/sidebar.d.ts +1 -0
- package/dist/clis/notion/sidebar.js +41 -0
- package/dist/clis/notion/status.d.ts +1 -0
- package/dist/clis/notion/status.js +16 -0
- package/dist/clis/notion/write.d.ts +1 -0
- package/dist/clis/notion/write.js +40 -0
- package/dist/clis/twitter/download.d.ts +8 -0
- package/dist/clis/twitter/download.js +204 -0
- package/dist/clis/wechat/chats.d.ts +1 -0
- package/dist/clis/wechat/chats.js +28 -0
- package/dist/clis/wechat/contacts.d.ts +1 -0
- package/dist/clis/wechat/contacts.js +28 -0
- package/dist/clis/wechat/read.d.ts +1 -0
- package/dist/clis/wechat/read.js +58 -0
- package/dist/clis/wechat/search.d.ts +1 -0
- package/dist/clis/wechat/search.js +31 -0
- package/dist/clis/wechat/send.d.ts +1 -0
- package/dist/clis/wechat/send.js +42 -0
- package/dist/clis/wechat/status.d.ts +1 -0
- package/dist/clis/wechat/status.js +29 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
- package/dist/clis/xiaohongshu/creator-notes.js +109 -0
- package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-profile.js +54 -0
- package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-stats.js +74 -0
- package/dist/clis/xiaohongshu/download.d.ts +7 -0
- package/dist/clis/xiaohongshu/download.js +155 -0
- package/dist/clis/xiaohongshu/search.js +1 -1
- package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
- package/dist/clis/xiaohongshu/user-helpers.js +67 -0
- package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
- package/dist/clis/xiaohongshu/user.js +46 -29
- package/dist/clis/zhihu/download.d.ts +11 -0
- package/dist/clis/zhihu/download.js +186 -0
- package/dist/clis/zhihu/download.test.d.ts +1 -0
- package/dist/clis/zhihu/download.test.js +10 -0
- package/dist/download/index.d.ts +79 -0
- package/dist/download/index.js +325 -0
- package/dist/download/progress.d.ts +36 -0
- package/dist/download/progress.js +111 -0
- package/dist/engine.test.js +15 -0
- package/dist/main.js +16 -3
- package/dist/pipeline/registry.js +2 -0
- package/dist/pipeline/steps/download.d.ts +34 -0
- package/dist/pipeline/steps/download.js +251 -0
- package/dist/pipeline/template.js +28 -0
- package/package.json +4 -3
- package/scripts/test-site.mjs +70 -0
- package/src/browser/discover.ts +23 -7
- package/src/browser.test.ts +23 -0
- package/src/build-manifest.test.ts +28 -0
- package/src/build-manifest.ts +147 -57
- package/src/clis/antigravity/README.md +2 -3
- package/src/clis/antigravity/README.zh-CN.md +2 -3
- package/src/clis/antigravity/SKILL.md +1 -1
- package/src/clis/antigravity/model.ts +2 -2
- package/src/clis/antigravity/send.ts +2 -2
- package/src/clis/bilibili/download.ts +161 -0
- package/src/clis/chatgpt/README.md +25 -16
- package/src/clis/chatgpt/README.zh-CN.md +27 -18
- package/src/clis/chatgpt/ask.ts +77 -0
- package/src/clis/chatgpt/send.ts +12 -0
- package/src/clis/chatwise/README.md +38 -0
- package/src/clis/chatwise/README.zh-CN.md +38 -0
- package/src/clis/chatwise/ask.ts +87 -0
- package/src/clis/chatwise/export.ts +51 -0
- package/src/clis/chatwise/history.ts +47 -0
- package/src/clis/chatwise/model.ts +87 -0
- package/src/clis/chatwise/new.ts +21 -0
- package/src/clis/chatwise/read.ts +42 -0
- package/src/clis/chatwise/screenshot.ts +33 -0
- package/src/clis/chatwise/send.ts +50 -0
- package/src/clis/chatwise/status.ts +25 -0
- package/src/clis/codex/ask.ts +77 -0
- package/src/clis/codex/export.ts +42 -0
- package/src/clis/codex/extract-diff.ts +1 -0
- package/src/clis/codex/history.ts +47 -0
- package/src/clis/codex/read.ts +5 -6
- package/src/clis/codex/screenshot.ts +33 -0
- package/src/clis/codex/send.ts +6 -7
- package/src/clis/codex/status.ts +4 -2
- package/src/clis/cursor/ask.ts +81 -0
- package/src/clis/cursor/composer.ts +9 -30
- package/src/clis/cursor/export.ts +57 -0
- package/src/clis/cursor/history.ts +47 -0
- package/src/clis/cursor/new.ts +4 -15
- package/src/clis/cursor/screenshot.ts +38 -0
- package/src/clis/discord-app/README.md +28 -0
- package/src/clis/discord-app/README.zh-CN.md +28 -0
- package/src/clis/discord-app/channels.ts +48 -0
- package/src/clis/discord-app/members.ts +41 -0
- package/src/clis/discord-app/read.ts +49 -0
- package/src/clis/discord-app/search.ts +64 -0
- package/src/clis/discord-app/send.ts +32 -0
- package/src/clis/discord-app/servers.ts +39 -0
- package/src/clis/discord-app/status.ts +18 -0
- package/src/clis/feishu/README.md +20 -0
- package/src/clis/feishu/README.zh-CN.md +20 -0
- package/src/clis/feishu/new.ts +32 -0
- package/src/clis/feishu/read.ts +48 -0
- package/src/clis/feishu/search.ts +35 -0
- package/src/clis/feishu/send.ts +46 -0
- package/src/clis/feishu/status.ts +34 -0
- package/src/clis/grok/ask.ts +90 -0
- package/src/clis/grok/debug.ts +49 -0
- package/src/clis/jimeng/generate.yaml +84 -0
- package/src/clis/jimeng/history.yaml +47 -0
- package/src/clis/linux-do/categories.yaml +41 -0
- package/src/clis/linux-do/category.yaml +49 -0
- package/src/clis/linux-do/hot.yaml +50 -0
- package/src/clis/linux-do/latest.yaml +40 -0
- package/src/clis/linux-do/search.yaml +45 -0
- package/src/clis/linux-do/topic.yaml +38 -0
- package/src/clis/notion/README.md +29 -0
- package/src/clis/notion/README.zh-CN.md +29 -0
- package/src/clis/notion/export.ts +36 -0
- package/src/clis/notion/favorites.ts +87 -0
- package/src/clis/notion/new.ts +39 -0
- package/src/clis/notion/read.ts +33 -0
- package/src/clis/notion/search.ts +54 -0
- package/src/clis/notion/sidebar.ts +44 -0
- package/src/clis/notion/status.ts +18 -0
- package/src/clis/notion/write.ts +45 -0
- package/src/clis/twitter/download.ts +227 -0
- package/src/clis/wechat/README.md +28 -0
- package/src/clis/wechat/README.zh-CN.md +28 -0
- package/src/clis/wechat/chats.ts +33 -0
- package/src/clis/wechat/contacts.ts +33 -0
- package/src/clis/wechat/read.ts +72 -0
- package/src/clis/wechat/search.ts +36 -0
- package/src/clis/wechat/send.ts +49 -0
- package/src/clis/wechat/status.ts +35 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
- package/src/clis/xiaohongshu/creator-notes.ts +116 -0
- package/src/clis/xiaohongshu/creator-profile.ts +60 -0
- package/src/clis/xiaohongshu/creator-stats.ts +81 -0
- package/src/clis/xiaohongshu/download.ts +173 -0
- package/src/clis/xiaohongshu/search.ts +1 -1
- package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
- package/src/clis/xiaohongshu/user-helpers.ts +85 -0
- package/src/clis/xiaohongshu/user.ts +52 -32
- package/src/clis/zhihu/download.test.ts +12 -0
- package/src/clis/zhihu/download.ts +223 -0
- package/src/download/index.ts +395 -0
- package/src/download/progress.ts +125 -0
- package/src/engine.test.ts +17 -0
- package/src/main.ts +12 -3
- package/src/pipeline/registry.ts +2 -0
- package/src/pipeline/steps/download.ts +310 -0
- package/src/pipeline/template.ts +26 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
export const sidebarCommand = cli({
|
|
3
|
+
site: 'notion',
|
|
4
|
+
name: 'sidebar',
|
|
5
|
+
description: 'List pages and databases from the Notion sidebar',
|
|
6
|
+
domain: 'localhost',
|
|
7
|
+
strategy: Strategy.UI,
|
|
8
|
+
browser: true,
|
|
9
|
+
args: [],
|
|
10
|
+
columns: ['Index', 'Title'],
|
|
11
|
+
func: async (page) => {
|
|
12
|
+
const items = await page.evaluate(`
|
|
13
|
+
(function() {
|
|
14
|
+
const results = [];
|
|
15
|
+
// Notion sidebar items
|
|
16
|
+
const selectors = [
|
|
17
|
+
'[class*="sidebar"] [role="treeitem"]',
|
|
18
|
+
'[class*="sidebar"] a',
|
|
19
|
+
'.notion-sidebar [role="button"]',
|
|
20
|
+
'nav [role="treeitem"]',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const sel of selectors) {
|
|
24
|
+
const nodes = document.querySelectorAll(sel);
|
|
25
|
+
if (nodes.length > 0) {
|
|
26
|
+
nodes.forEach((n, i) => {
|
|
27
|
+
const text = (n.textContent || '').trim().substring(0, 100);
|
|
28
|
+
if (text && text.length > 1) results.push({ Index: i + 1, Title: text });
|
|
29
|
+
});
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
})()
|
|
35
|
+
`);
|
|
36
|
+
if (items.length === 0) {
|
|
37
|
+
return [{ Index: 0, Title: 'No sidebar items found. Toggle the sidebar first.' }];
|
|
38
|
+
}
|
|
39
|
+
return items;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const statusCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
export const statusCommand = cli({
|
|
3
|
+
site: 'notion',
|
|
4
|
+
name: 'status',
|
|
5
|
+
description: 'Check active CDP connection to Notion Desktop',
|
|
6
|
+
domain: 'localhost',
|
|
7
|
+
strategy: Strategy.UI,
|
|
8
|
+
browser: true,
|
|
9
|
+
args: [],
|
|
10
|
+
columns: ['Status', 'Url', 'Title'],
|
|
11
|
+
func: async (page) => {
|
|
12
|
+
const url = await page.evaluate('window.location.href');
|
|
13
|
+
const title = await page.evaluate('document.title');
|
|
14
|
+
return [{ Status: 'Connected', Url: url, Title: title }];
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const writeCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
export const writeCommand = cli({
|
|
3
|
+
site: 'notion',
|
|
4
|
+
name: 'write',
|
|
5
|
+
description: 'Append text content to the currently open Notion page',
|
|
6
|
+
domain: 'localhost',
|
|
7
|
+
strategy: Strategy.UI,
|
|
8
|
+
browser: true,
|
|
9
|
+
args: [{ name: 'text', required: true, positional: true, help: 'Text to append to the page' }],
|
|
10
|
+
columns: ['Status'],
|
|
11
|
+
func: async (page, kwargs) => {
|
|
12
|
+
const text = kwargs.text;
|
|
13
|
+
// Focus the page body and move to the end
|
|
14
|
+
await page.evaluate(`
|
|
15
|
+
(function(text) {
|
|
16
|
+
// Find the editable area in Notion
|
|
17
|
+
const editables = document.querySelectorAll('.notion-page-content [contenteditable="true"], [class*="page-content"] [contenteditable="true"]');
|
|
18
|
+
let target = editables.length > 0 ? editables[editables.length - 1] : null;
|
|
19
|
+
|
|
20
|
+
if (!target) {
|
|
21
|
+
// Fallback: just find any contenteditable
|
|
22
|
+
const all = document.querySelectorAll('[contenteditable="true"]');
|
|
23
|
+
target = all.length > 0 ? all[all.length - 1] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!target) throw new Error('Could not find editable area in Notion page');
|
|
27
|
+
|
|
28
|
+
target.focus();
|
|
29
|
+
// Move to end
|
|
30
|
+
const sel = window.getSelection();
|
|
31
|
+
sel.selectAllChildren(target);
|
|
32
|
+
sel.collapseToEnd();
|
|
33
|
+
|
|
34
|
+
document.execCommand('insertText', false, text);
|
|
35
|
+
})(${JSON.stringify(text)})
|
|
36
|
+
`);
|
|
37
|
+
await page.wait(0.5);
|
|
38
|
+
return [{ Status: 'Text appended successfully' }];
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twitter/X download — download images and videos from tweets.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* opencli twitter download --username elonmusk --limit 10 --output ./twitter
|
|
6
|
+
* opencli twitter download --tweet-url https://x.com/xxx/status/123 --output ./twitter
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twitter/X download — download images and videos from tweets.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* opencli twitter download --username elonmusk --limit 10 --output ./twitter
|
|
6
|
+
* opencli twitter download --tweet-url https://x.com/xxx/status/123 --output ./twitter
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { cli, Strategy } from '../../registry.js';
|
|
11
|
+
import { httpDownload, ytdlpDownload, checkYtdlp, getTempDir, exportCookiesToNetscape, } from '../../download/index.js';
|
|
12
|
+
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
13
|
+
cli({
|
|
14
|
+
site: 'twitter',
|
|
15
|
+
name: 'download',
|
|
16
|
+
description: '下载 Twitter/X 媒体(图片和视频)',
|
|
17
|
+
domain: 'x.com',
|
|
18
|
+
strategy: Strategy.COOKIE,
|
|
19
|
+
args: [
|
|
20
|
+
{ name: 'username', help: 'Twitter username (downloads from media tab)' },
|
|
21
|
+
{ name: 'tweet-url', help: 'Single tweet URL to download' },
|
|
22
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of tweets to scan' },
|
|
23
|
+
{ name: 'output', default: './twitter-downloads', help: 'Output directory' },
|
|
24
|
+
],
|
|
25
|
+
columns: ['index', 'type', 'status', 'size'],
|
|
26
|
+
func: async (page, kwargs) => {
|
|
27
|
+
const username = kwargs.username;
|
|
28
|
+
const tweetUrl = kwargs['tweet-url'];
|
|
29
|
+
const limit = kwargs.limit;
|
|
30
|
+
const output = kwargs.output;
|
|
31
|
+
if (!username && !tweetUrl) {
|
|
32
|
+
return [{
|
|
33
|
+
index: 0,
|
|
34
|
+
type: '-',
|
|
35
|
+
status: 'failed',
|
|
36
|
+
size: 'Must provide --username or --tweet-url',
|
|
37
|
+
}];
|
|
38
|
+
}
|
|
39
|
+
// Navigate to the appropriate page
|
|
40
|
+
if (tweetUrl) {
|
|
41
|
+
await page.goto(tweetUrl);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
await page.goto(`https://x.com/${username}/media`);
|
|
45
|
+
}
|
|
46
|
+
await page.wait(3);
|
|
47
|
+
// Scroll to load more content
|
|
48
|
+
if (!tweetUrl) {
|
|
49
|
+
await page.autoScroll({ times: Math.ceil(limit / 5) });
|
|
50
|
+
}
|
|
51
|
+
// Extract media URLs
|
|
52
|
+
const data = await page.evaluate(`
|
|
53
|
+
(() => {
|
|
54
|
+
const media = [];
|
|
55
|
+
|
|
56
|
+
// Find images (high quality)
|
|
57
|
+
document.querySelectorAll('img[src*="pbs.twimg.com/media"]').forEach(img => {
|
|
58
|
+
let src = img.src || '';
|
|
59
|
+
// Get large version
|
|
60
|
+
src = src.replace(/&name=\\w+$/, '&name=large');
|
|
61
|
+
src = src.replace(/\\?format=/, '?format=');
|
|
62
|
+
if (!src.includes('&name=')) {
|
|
63
|
+
src = src + '&name=large';
|
|
64
|
+
}
|
|
65
|
+
media.push({ type: 'image', url: src });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Find videos
|
|
69
|
+
document.querySelectorAll('video').forEach(video => {
|
|
70
|
+
const src = video.src || '';
|
|
71
|
+
if (src) {
|
|
72
|
+
media.push({ type: 'video', url: src, poster: video.poster || '' });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Find video tweets (for yt-dlp)
|
|
77
|
+
document.querySelectorAll('[data-testid="videoPlayer"]').forEach(player => {
|
|
78
|
+
const tweetLink = player.closest('article')?.querySelector('a[href*="/status/"]');
|
|
79
|
+
const href = tweetLink?.getAttribute('href') || '';
|
|
80
|
+
if (href) {
|
|
81
|
+
const tweetUrl = 'https://x.com' + href;
|
|
82
|
+
media.push({ type: 'video-tweet', url: tweetUrl });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return media;
|
|
87
|
+
})()
|
|
88
|
+
`);
|
|
89
|
+
if (!data || data.length === 0) {
|
|
90
|
+
return [{
|
|
91
|
+
index: 0,
|
|
92
|
+
type: '-',
|
|
93
|
+
status: 'failed',
|
|
94
|
+
size: 'No media found',
|
|
95
|
+
}];
|
|
96
|
+
}
|
|
97
|
+
// Extract cookies
|
|
98
|
+
const cookieString = await page.evaluate(`(() => document.cookie)()`);
|
|
99
|
+
// Create output directory
|
|
100
|
+
const outputDir = tweetUrl
|
|
101
|
+
? path.join(output, 'tweets')
|
|
102
|
+
: path.join(output, username || 'media');
|
|
103
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
104
|
+
// Export cookies for yt-dlp
|
|
105
|
+
let cookiesFile;
|
|
106
|
+
if (typeof cookieString === 'string' && cookieString) {
|
|
107
|
+
const tempDir = getTempDir();
|
|
108
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
109
|
+
cookiesFile = path.join(tempDir, `twitter_cookies_${Date.now()}.txt`);
|
|
110
|
+
const cookies = cookieString.split(';').map((c) => {
|
|
111
|
+
const [name, ...rest] = c.trim().split('=');
|
|
112
|
+
return {
|
|
113
|
+
name: name || '',
|
|
114
|
+
value: rest.join('=') || '',
|
|
115
|
+
domain: '.x.com',
|
|
116
|
+
path: '/',
|
|
117
|
+
secure: true,
|
|
118
|
+
httpOnly: false,
|
|
119
|
+
};
|
|
120
|
+
}).filter((c) => c.name);
|
|
121
|
+
exportCookiesToNetscape(cookies, cookiesFile);
|
|
122
|
+
}
|
|
123
|
+
// Deduplicate media
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const uniqueMedia = data.filter((m) => {
|
|
126
|
+
if (seen.has(m.url))
|
|
127
|
+
return false;
|
|
128
|
+
seen.add(m.url);
|
|
129
|
+
return true;
|
|
130
|
+
}).slice(0, limit);
|
|
131
|
+
const tracker = new DownloadProgressTracker(uniqueMedia.length, true);
|
|
132
|
+
const results = [];
|
|
133
|
+
for (let i = 0; i < uniqueMedia.length; i++) {
|
|
134
|
+
const media = uniqueMedia[i];
|
|
135
|
+
const ext = media.type === 'image' ? 'jpg' : 'mp4';
|
|
136
|
+
const filename = `${username || 'tweet'}_${i + 1}.${ext}`;
|
|
137
|
+
const destPath = path.join(outputDir, filename);
|
|
138
|
+
const progressBar = tracker.onFileStart(filename, i);
|
|
139
|
+
try {
|
|
140
|
+
let result;
|
|
141
|
+
if (media.type === 'video-tweet' && checkYtdlp()) {
|
|
142
|
+
// Use yt-dlp for video tweets
|
|
143
|
+
result = await ytdlpDownload(media.url, destPath, {
|
|
144
|
+
cookiesFile,
|
|
145
|
+
extraArgs: ['--merge-output-format', 'mp4'],
|
|
146
|
+
onProgress: (percent) => {
|
|
147
|
+
if (progressBar)
|
|
148
|
+
progressBar.update(percent, 100);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else if (media.type === 'image') {
|
|
153
|
+
// Direct HTTP download for images
|
|
154
|
+
result = await httpDownload(media.url, destPath, {
|
|
155
|
+
cookies: typeof cookieString === 'string' ? cookieString : '',
|
|
156
|
+
timeout: 30000,
|
|
157
|
+
onProgress: (received, total) => {
|
|
158
|
+
if (progressBar)
|
|
159
|
+
progressBar.update(received, total);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Direct HTTP download for direct video URLs
|
|
165
|
+
result = await httpDownload(media.url, destPath, {
|
|
166
|
+
cookies: typeof cookieString === 'string' ? cookieString : '',
|
|
167
|
+
timeout: 60000,
|
|
168
|
+
onProgress: (received, total) => {
|
|
169
|
+
if (progressBar)
|
|
170
|
+
progressBar.update(received, total);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (progressBar) {
|
|
175
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
176
|
+
}
|
|
177
|
+
tracker.onFileComplete(result.success);
|
|
178
|
+
results.push({
|
|
179
|
+
index: i + 1,
|
|
180
|
+
type: media.type === 'video-tweet' ? 'video' : media.type,
|
|
181
|
+
status: result.success ? 'success' : 'failed',
|
|
182
|
+
size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
if (progressBar)
|
|
187
|
+
progressBar.fail(err.message);
|
|
188
|
+
tracker.onFileComplete(false);
|
|
189
|
+
results.push({
|
|
190
|
+
index: i + 1,
|
|
191
|
+
type: media.type,
|
|
192
|
+
status: 'failed',
|
|
193
|
+
size: err.message,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
tracker.finish();
|
|
198
|
+
// Cleanup cookies file
|
|
199
|
+
if (cookiesFile && fs.existsSync(cookiesFile)) {
|
|
200
|
+
fs.unlinkSync(cookiesFile);
|
|
201
|
+
}
|
|
202
|
+
return results;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const chatsCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
export const chatsCommand = cli({
|
|
4
|
+
site: 'wechat',
|
|
5
|
+
name: 'chats',
|
|
6
|
+
description: 'Open the WeChat chats panel (conversation list)',
|
|
7
|
+
domain: 'localhost',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [],
|
|
11
|
+
columns: ['Status'],
|
|
12
|
+
func: async (page) => {
|
|
13
|
+
try {
|
|
14
|
+
// Activate WeChat
|
|
15
|
+
execSync("osascript -e 'tell application \"WeChat\" to activate'");
|
|
16
|
+
execSync("osascript -e 'delay 0.3'");
|
|
17
|
+
// Cmd+1 switches to Chats tab in WeChat Mac
|
|
18
|
+
execSync("osascript " +
|
|
19
|
+
"-e 'tell application \"System Events\"' " +
|
|
20
|
+
"-e 'keystroke \"1\" using command down' " +
|
|
21
|
+
"-e 'end tell'");
|
|
22
|
+
return [{ Status: 'Chats panel opened (Cmd+1)' }];
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return [{ Status: 'Error: ' + err.message }];
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const contactsCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
export const contactsCommand = cli({
|
|
4
|
+
site: 'wechat',
|
|
5
|
+
name: 'contacts',
|
|
6
|
+
description: 'Open the WeChat contacts panel',
|
|
7
|
+
domain: 'localhost',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [],
|
|
11
|
+
columns: ['Status'],
|
|
12
|
+
func: async (page) => {
|
|
13
|
+
try {
|
|
14
|
+
// Activate WeChat
|
|
15
|
+
execSync("osascript -e 'tell application \"WeChat\" to activate'");
|
|
16
|
+
execSync("osascript -e 'delay 0.3'");
|
|
17
|
+
// Cmd+2 switches to Contacts tab in WeChat Mac
|
|
18
|
+
execSync("osascript " +
|
|
19
|
+
"-e 'tell application \"System Events\"' " +
|
|
20
|
+
"-e 'keystroke \"2\" using command down' " +
|
|
21
|
+
"-e 'end tell'");
|
|
22
|
+
return [{ Status: 'Contacts panel opened (Cmd+2)' }];
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return [{ Status: 'Error: ' + err.message }];
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const readCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
export const readCommand = cli({
|
|
4
|
+
site: 'wechat',
|
|
5
|
+
name: 'read',
|
|
6
|
+
description: 'Read the current chat content by selecting all and copying',
|
|
7
|
+
domain: 'localhost',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [],
|
|
11
|
+
columns: ['Content'],
|
|
12
|
+
func: async (page) => {
|
|
13
|
+
try {
|
|
14
|
+
// Backup clipboard
|
|
15
|
+
let clipBackup = '';
|
|
16
|
+
try {
|
|
17
|
+
clipBackup = execSync('pbpaste', { encoding: 'utf-8' });
|
|
18
|
+
}
|
|
19
|
+
catch { /* empty */ }
|
|
20
|
+
// Activate WeChat
|
|
21
|
+
execSync("osascript -e 'tell application \"WeChat\" to activate'");
|
|
22
|
+
execSync("osascript -e 'delay 0.3'");
|
|
23
|
+
// Click on the chat area first, then select all and copy
|
|
24
|
+
execSync("osascript " +
|
|
25
|
+
"-e 'tell application \"System Events\"' " +
|
|
26
|
+
"-e 'tell application process \"WeChat\"' " +
|
|
27
|
+
// Click in the message area (center-right of the window)
|
|
28
|
+
"-e 'set frontWin to front window' " +
|
|
29
|
+
"-e 'set winPos to position of frontWin' " +
|
|
30
|
+
"-e 'set winSize to size of frontWin' " +
|
|
31
|
+
"-e 'end tell' " +
|
|
32
|
+
"-e 'end tell'");
|
|
33
|
+
execSync("osascript -e 'delay 0.2'");
|
|
34
|
+
// Select all text in chat area and copy
|
|
35
|
+
execSync("osascript " +
|
|
36
|
+
"-e 'tell application \"System Events\"' " +
|
|
37
|
+
"-e 'keystroke \"a\" using command down' " +
|
|
38
|
+
"-e 'delay 0.2' " +
|
|
39
|
+
"-e 'keystroke \"c\" using command down' " +
|
|
40
|
+
"-e 'delay 0.2' " +
|
|
41
|
+
"-e 'end tell'");
|
|
42
|
+
const content = execSync('pbpaste', { encoding: 'utf-8' }).trim();
|
|
43
|
+
// Restore clipboard
|
|
44
|
+
if (clipBackup) {
|
|
45
|
+
spawnSync('pbcopy', { input: clipBackup });
|
|
46
|
+
}
|
|
47
|
+
// Press Escape to deselect
|
|
48
|
+
execSync("osascript " +
|
|
49
|
+
"-e 'tell application \"System Events\"' " +
|
|
50
|
+
"-e 'key code 53' " + // Escape
|
|
51
|
+
"-e 'end tell'");
|
|
52
|
+
return [{ Content: content || '(no content captured)' }];
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return [{ Content: 'Error: ' + err.message }];
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const searchCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
export const searchCommand = cli({
|
|
4
|
+
site: 'wechat',
|
|
5
|
+
name: 'search',
|
|
6
|
+
description: 'Open WeChat search and type a query (find contacts or messages)',
|
|
7
|
+
domain: 'localhost',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [{ name: 'query', required: true, positional: true, help: 'Search query (contact name or keyword)' }],
|
|
11
|
+
columns: ['Status'],
|
|
12
|
+
func: async (page, kwargs) => {
|
|
13
|
+
const query = kwargs.query;
|
|
14
|
+
try {
|
|
15
|
+
// Activate WeChat
|
|
16
|
+
execSync("osascript -e 'tell application \"WeChat\" to activate'");
|
|
17
|
+
execSync("osascript -e 'delay 0.3'");
|
|
18
|
+
// Cmd+F to open search (WeChat Mac uses Cmd+F for search)
|
|
19
|
+
execSync("osascript " +
|
|
20
|
+
"-e 'tell application \"System Events\"' " +
|
|
21
|
+
"-e 'keystroke \"f\" using command down' " +
|
|
22
|
+
"-e 'delay 0.5' " +
|
|
23
|
+
`-e 'keystroke ${JSON.stringify(query)}' ` +
|
|
24
|
+
"-e 'end tell'");
|
|
25
|
+
return [{ Status: `Searching for: ${query}` }];
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return [{ Status: 'Error: ' + err.message }];
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sendCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
export const sendCommand = cli({
|
|
4
|
+
site: 'wechat',
|
|
5
|
+
name: 'send',
|
|
6
|
+
description: 'Send a message in the active WeChat conversation via clipboard paste',
|
|
7
|
+
domain: 'localhost',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
|
|
11
|
+
columns: ['Status'],
|
|
12
|
+
func: async (page, kwargs) => {
|
|
13
|
+
const text = kwargs.text;
|
|
14
|
+
try {
|
|
15
|
+
// Backup clipboard
|
|
16
|
+
let clipBackup = '';
|
|
17
|
+
try {
|
|
18
|
+
clipBackup = execSync('pbpaste', { encoding: 'utf-8' });
|
|
19
|
+
}
|
|
20
|
+
catch { /* clipboard may be empty */ }
|
|
21
|
+
// Copy text to clipboard
|
|
22
|
+
spawnSync('pbcopy', { input: text });
|
|
23
|
+
// Activate WeChat and paste
|
|
24
|
+
execSync("osascript -e 'tell application \"WeChat\" to activate'");
|
|
25
|
+
execSync("osascript -e 'delay 0.5'");
|
|
26
|
+
execSync("osascript " +
|
|
27
|
+
"-e 'tell application \"System Events\"' " +
|
|
28
|
+
"-e 'keystroke \"v\" using command down' " +
|
|
29
|
+
"-e 'delay 0.2' " +
|
|
30
|
+
"-e 'keystroke return' " +
|
|
31
|
+
"-e 'end tell'");
|
|
32
|
+
// Restore clipboard
|
|
33
|
+
if (clipBackup) {
|
|
34
|
+
spawnSync('pbcopy', { input: clipBackup });
|
|
35
|
+
}
|
|
36
|
+
return [{ Status: 'Success' }];
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return [{ Status: 'Error: ' + err.message }];
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const statusCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
export const statusCommand = cli({
|
|
4
|
+
site: 'wechat',
|
|
5
|
+
name: 'status',
|
|
6
|
+
description: 'Check if WeChat Desktop is running on macOS',
|
|
7
|
+
domain: 'localhost',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [],
|
|
11
|
+
columns: ['Status', 'Detail'],
|
|
12
|
+
func: async (page) => {
|
|
13
|
+
try {
|
|
14
|
+
const running = execSync("osascript -e 'application \"WeChat\" is running'", { encoding: 'utf-8' }).trim();
|
|
15
|
+
if (running !== 'true') {
|
|
16
|
+
return [{ Status: 'Stopped', Detail: 'WeChat is not running' }];
|
|
17
|
+
}
|
|
18
|
+
// Get window count to check if logged in
|
|
19
|
+
const windowCount = execSync("osascript -e 'tell application \"System Events\" to count windows of application process \"WeChat\"'", { encoding: 'utf-8' }).trim();
|
|
20
|
+
return [{
|
|
21
|
+
Status: 'Running',
|
|
22
|
+
Detail: `${windowCount} window(s) open`,
|
|
23
|
+
}];
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return [{ Status: 'Error', Detail: err.message }];
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaohongshu Creator Note Detail — per-note analytics breakdown.
|
|
3
|
+
*
|
|
4
|
+
* Uses the creator.xiaohongshu.com internal API (cookie auth).
|
|
5
|
+
* Returns total reads, engagement, likes, collects, comments, shares
|
|
6
|
+
* for a specific note, split by channel (organic vs promoted vs video).
|
|
7
|
+
*
|
|
8
|
+
* Requires: logged into creator.xiaohongshu.com in Chrome.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaohongshu Creator Note Detail — per-note analytics breakdown.
|
|
3
|
+
*
|
|
4
|
+
* Uses the creator.xiaohongshu.com internal API (cookie auth).
|
|
5
|
+
* Returns total reads, engagement, likes, collects, comments, shares
|
|
6
|
+
* for a specific note, split by channel (organic vs promoted vs video).
|
|
7
|
+
*
|
|
8
|
+
* Requires: logged into creator.xiaohongshu.com in Chrome.
|
|
9
|
+
*/
|
|
10
|
+
import { cli, Strategy } from '../../registry.js';
|
|
11
|
+
cli({
|
|
12
|
+
site: 'xiaohongshu',
|
|
13
|
+
name: 'creator-note-detail',
|
|
14
|
+
description: '小红书单篇笔记详细数据 (阅读/互动/点赞/收藏/评论/分享,区分自然流量/推广/视频)',
|
|
15
|
+
domain: 'creator.xiaohongshu.com',
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
browser: true,
|
|
18
|
+
args: [
|
|
19
|
+
{ name: 'note_id', type: 'string', required: true, help: 'Note ID (from note URL or creator-notes command)' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['channel', 'reads', 'engagement', 'likes', 'collects', 'comments', 'shares'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const noteId = kwargs.note_id;
|
|
24
|
+
const encodedNoteId = encodeURIComponent(noteId);
|
|
25
|
+
// Navigate for cookie context
|
|
26
|
+
await page.goto('https://creator.xiaohongshu.com/new/home');
|
|
27
|
+
await page.wait(2);
|
|
28
|
+
const data = await page.evaluate(`
|
|
29
|
+
async () => {
|
|
30
|
+
try {
|
|
31
|
+
const resp = await fetch(
|
|
32
|
+
'/api/galaxy/creator/data/note_detail?note_id=${encodedNoteId}',
|
|
33
|
+
{ credentials: 'include' }
|
|
34
|
+
);
|
|
35
|
+
if (!resp.ok) return { error: 'HTTP ' + resp.status };
|
|
36
|
+
return await resp.json();
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return { error: e.message };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`);
|
|
42
|
+
if (data?.error) {
|
|
43
|
+
throw new Error(data.error + '. Check note_id and login status.');
|
|
44
|
+
}
|
|
45
|
+
if (!data?.data) {
|
|
46
|
+
throw new Error('Unexpected response structure');
|
|
47
|
+
}
|
|
48
|
+
const d = data.data;
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
channel: 'Total',
|
|
52
|
+
reads: d.total_read ?? 0,
|
|
53
|
+
engagement: d.total_engage ?? 0,
|
|
54
|
+
likes: d.total_like ?? 0,
|
|
55
|
+
collects: d.total_fav ?? 0,
|
|
56
|
+
comments: d.total_cmt ?? 0,
|
|
57
|
+
shares: d.total_share ?? 0,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
channel: 'Organic',
|
|
61
|
+
reads: d.normal_read ?? 0,
|
|
62
|
+
engagement: d.normal_engage ?? 0,
|
|
63
|
+
likes: d.normal_like ?? 0,
|
|
64
|
+
collects: d.normal_fav ?? 0,
|
|
65
|
+
comments: d.normal_cmt ?? 0,
|
|
66
|
+
shares: d.normal_share ?? 0,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
channel: 'Promoted',
|
|
70
|
+
reads: d.total_promo_read ?? 0,
|
|
71
|
+
engagement: 0,
|
|
72
|
+
likes: 0,
|
|
73
|
+
collects: 0,
|
|
74
|
+
comments: 0,
|
|
75
|
+
shares: 0,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
channel: 'Video',
|
|
79
|
+
reads: d.video_read ?? 0,
|
|
80
|
+
engagement: d.video_engage ?? 0,
|
|
81
|
+
likes: d.video_like ?? 0,
|
|
82
|
+
collects: d.video_fav ?? 0,
|
|
83
|
+
comments: d.video_cmt ?? 0,
|
|
84
|
+
shares: d.video_share ?? 0,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
},
|
|
88
|
+
});
|