@jackwener/opencli 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/actions/setup-chrome/action.yml +5 -4
- package/.github/workflows/ci.yml +17 -3
- package/.github/workflows/e2e-headed.yml +16 -3
- package/CHANGELOG.md +23 -0
- package/PRIVACY.md +57 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SKILL.md +101 -2
- package/dist/cli-manifest.json +720 -32
- package/dist/clis/apple-podcasts/search.js +2 -1
- package/dist/clis/arxiv/search.js +2 -2
- package/dist/clis/bbc/news.js +0 -1
- package/dist/clis/ctrip/search.js +0 -1
- package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
- package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
- package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
- package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
- package/dist/clis/douyin/_shared/creation-id.js +5 -0
- package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
- package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
- package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
- package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
- package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
- package/dist/clis/douyin/_shared/sts2.js +15 -0
- package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
- package/dist/clis/douyin/_shared/text-extra.js +15 -0
- package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
- package/dist/clis/douyin/_shared/timing.d.ts +2 -0
- package/dist/clis/douyin/_shared/timing.js +22 -0
- package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/timing.test.js +28 -0
- package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
- package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
- package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
- package/dist/clis/douyin/_shared/tos-upload.js +295 -0
- package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
- package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
- package/dist/clis/douyin/_shared/transcode.js +45 -0
- package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/transcode.test.js +93 -0
- package/dist/clis/douyin/_shared/types.d.ts +26 -0
- package/dist/clis/douyin/_shared/types.js +1 -0
- package/dist/clis/douyin/activities.d.ts +1 -0
- package/dist/clis/douyin/activities.js +20 -0
- package/dist/clis/douyin/activities.test.d.ts +1 -0
- package/dist/clis/douyin/activities.test.js +22 -0
- package/dist/clis/douyin/collections.d.ts +1 -0
- package/dist/clis/douyin/collections.js +22 -0
- package/dist/clis/douyin/collections.test.d.ts +1 -0
- package/dist/clis/douyin/collections.test.js +23 -0
- package/dist/clis/douyin/delete.d.ts +1 -0
- package/dist/clis/douyin/delete.js +18 -0
- package/dist/clis/douyin/delete.test.d.ts +1 -0
- package/dist/clis/douyin/delete.test.js +11 -0
- package/dist/clis/douyin/draft.d.ts +14 -0
- package/dist/clis/douyin/draft.js +237 -0
- package/dist/clis/douyin/draft.test.d.ts +1 -0
- package/dist/clis/douyin/draft.test.js +11 -0
- package/dist/clis/douyin/drafts.d.ts +1 -0
- package/dist/clis/douyin/drafts.js +23 -0
- package/dist/clis/douyin/drafts.test.d.ts +1 -0
- package/dist/clis/douyin/drafts.test.js +11 -0
- package/dist/clis/douyin/hashtag.d.ts +1 -0
- package/dist/clis/douyin/hashtag.js +45 -0
- package/dist/clis/douyin/hashtag.test.d.ts +1 -0
- package/dist/clis/douyin/hashtag.test.js +25 -0
- package/dist/clis/douyin/location.d.ts +1 -0
- package/dist/clis/douyin/location.js +24 -0
- package/dist/clis/douyin/location.test.d.ts +1 -0
- package/dist/clis/douyin/location.test.js +23 -0
- package/dist/clis/douyin/profile.d.ts +1 -0
- package/dist/clis/douyin/profile.js +28 -0
- package/dist/clis/douyin/profile.test.d.ts +1 -0
- package/dist/clis/douyin/profile.test.js +11 -0
- package/dist/clis/douyin/publish.d.ts +14 -0
- package/dist/clis/douyin/publish.js +288 -0
- package/dist/clis/douyin/publish.test.d.ts +1 -0
- package/dist/clis/douyin/publish.test.js +38 -0
- package/dist/clis/douyin/stats.d.ts +1 -0
- package/dist/clis/douyin/stats.js +27 -0
- package/dist/clis/douyin/stats.test.d.ts +1 -0
- package/dist/clis/douyin/stats.test.js +22 -0
- package/dist/clis/douyin/update.d.ts +1 -0
- package/dist/clis/douyin/update.js +31 -0
- package/dist/clis/douyin/update.test.d.ts +1 -0
- package/dist/clis/douyin/update.test.js +11 -0
- package/dist/clis/douyin/videos.d.ts +1 -0
- package/dist/clis/douyin/videos.js +34 -0
- package/dist/clis/douyin/videos.test.d.ts +1 -0
- package/dist/clis/douyin/videos.test.js +11 -0
- package/dist/clis/hackernews/search.yaml +1 -1
- package/dist/clis/instagram/search.yaml +2 -1
- package/dist/clis/linux-do/search.yaml +3 -1
- package/dist/clis/medium/search.js +1 -1
- package/dist/clis/reuters/search.js +0 -1
- package/dist/clis/twitter/search.js +5 -3
- package/dist/clis/twitter/search.test.js +54 -2
- package/dist/clis/weibo/comments.d.ts +1 -0
- package/dist/clis/weibo/comments.js +53 -0
- package/dist/clis/weibo/feed.d.ts +1 -0
- package/dist/clis/weibo/feed.js +56 -0
- package/dist/clis/weibo/hot.js +0 -1
- package/dist/clis/weibo/me.d.ts +1 -0
- package/dist/clis/weibo/me.js +76 -0
- package/dist/clis/weibo/post.d.ts +1 -0
- package/dist/clis/weibo/post.js +75 -0
- package/dist/clis/weibo/user.d.ts +1 -0
- package/dist/clis/weibo/user.js +63 -0
- package/dist/clis/weibo/utils.d.ts +6 -0
- package/dist/clis/weibo/utils.js +30 -0
- package/dist/clis/weread/search.js +3 -2
- package/dist/clis/xueqiu/search.yaml +2 -1
- package/dist/clis/yahoo-finance/quote.js +0 -1
- package/dist/clis/youtube/channel.d.ts +1 -0
- package/dist/clis/youtube/channel.js +150 -0
- package/dist/clis/youtube/comments.d.ts +1 -0
- package/dist/clis/youtube/comments.js +95 -0
- package/dist/clis/youtube/search.js +0 -1
- package/dist/clis/zhihu/search.yaml +2 -1
- package/dist/external-clis.yaml +0 -17
- package/dist/weread-search-regression.test.d.ts +1 -0
- package/dist/weread-search-regression.test.js +39 -0
- package/docs/.vitepress/config.mts +13 -0
- package/docs/adapters/browser/douyin.md +75 -0
- package/docs/adapters/browser/twitter.md +6 -0
- package/docs/adapters/index.md +6 -1
- package/extension/dist/background.js +508 -518
- package/extension/manifest.json +6 -2
- package/extension/package.json +1 -1
- package/extension/popup.html +84 -0
- package/extension/popup.js +25 -0
- package/extension/src/background.ts +20 -1
- package/package.json +1 -1
- package/src/clis/apple-podcasts/search.ts +2 -1
- package/src/clis/arxiv/search.ts +2 -2
- package/src/clis/bbc/news.ts +0 -1
- package/src/clis/ctrip/search.ts +0 -1
- package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
- package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
- package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
- package/src/clis/douyin/_shared/creation-id.ts +8 -0
- package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
- package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
- package/src/clis/douyin/_shared/sts2.ts +20 -0
- package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
- package/src/clis/douyin/_shared/text-extra.ts +33 -0
- package/src/clis/douyin/_shared/timing.test.ts +38 -0
- package/src/clis/douyin/_shared/timing.ts +22 -0
- package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
- package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
- package/src/clis/douyin/_shared/tos-upload.ts +444 -0
- package/src/clis/douyin/_shared/transcode.test.ts +117 -0
- package/src/clis/douyin/_shared/transcode.ts +78 -0
- package/src/clis/douyin/_shared/types.ts +29 -0
- package/src/clis/douyin/activities.test.ts +25 -0
- package/src/clis/douyin/activities.ts +23 -0
- package/src/clis/douyin/collections.test.ts +26 -0
- package/src/clis/douyin/collections.ts +25 -0
- package/src/clis/douyin/delete.test.ts +12 -0
- package/src/clis/douyin/delete.ts +20 -0
- package/src/clis/douyin/draft.test.ts +12 -0
- package/src/clis/douyin/draft.ts +282 -0
- package/src/clis/douyin/drafts.test.ts +12 -0
- package/src/clis/douyin/drafts.ts +27 -0
- package/src/clis/douyin/hashtag.test.ts +28 -0
- package/src/clis/douyin/hashtag.ts +56 -0
- package/src/clis/douyin/location.test.ts +26 -0
- package/src/clis/douyin/location.ts +27 -0
- package/src/clis/douyin/profile.test.ts +12 -0
- package/src/clis/douyin/profile.ts +37 -0
- package/src/clis/douyin/publish.test.ts +45 -0
- package/src/clis/douyin/publish.ts +340 -0
- package/src/clis/douyin/stats.test.ts +25 -0
- package/src/clis/douyin/stats.ts +30 -0
- package/src/clis/douyin/update.test.ts +12 -0
- package/src/clis/douyin/update.ts +43 -0
- package/src/clis/douyin/videos.test.ts +12 -0
- package/src/clis/douyin/videos.ts +49 -0
- package/src/clis/hackernews/search.yaml +1 -1
- package/src/clis/instagram/search.yaml +2 -1
- package/src/clis/linux-do/search.yaml +3 -1
- package/src/clis/medium/search.ts +1 -1
- package/src/clis/reuters/search.ts +0 -1
- package/src/clis/twitter/search.test.ts +69 -2
- package/src/clis/twitter/search.ts +5 -3
- package/src/clis/weibo/comments.ts +54 -0
- package/src/clis/weibo/feed.ts +57 -0
- package/src/clis/weibo/hot.ts +0 -1
- package/src/clis/weibo/me.ts +77 -0
- package/src/clis/weibo/post.ts +77 -0
- package/src/clis/weibo/user.ts +64 -0
- package/src/clis/weibo/utils.ts +32 -0
- package/src/clis/weread/search.ts +3 -2
- package/src/clis/xueqiu/search.yaml +2 -1
- package/src/clis/yahoo-finance/quote.ts +0 -1
- package/src/clis/youtube/channel.ts +155 -0
- package/src/clis/youtube/comments.ts +97 -0
- package/src/clis/youtube/search.ts +0 -1
- package/src/clis/zhihu/search.yaml +2 -1
- package/src/external-clis.yaml +0 -17
- package/src/weread-search-regression.test.ts +44 -0
- package/tests/e2e/browser-public-extended.test.ts +162 -0
- package/tests/e2e/browser-public.test.ts +7 -146
- package/vitest.config.ts +24 -17
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube channel — get channel info and recent videos via InnerTube API.
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
6
|
+
cli({
|
|
7
|
+
site: 'youtube',
|
|
8
|
+
name: 'channel',
|
|
9
|
+
description: 'Get YouTube channel info and recent videos',
|
|
10
|
+
domain: 'www.youtube.com',
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'id', required: true, positional: true, help: 'Channel ID (UCxxxx) or handle (@name)' },
|
|
14
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Max recent videos (max 30)' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['field', 'value'],
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
const channelId = String(kwargs.id);
|
|
19
|
+
const limit = Math.min(kwargs.limit || 10, 30);
|
|
20
|
+
await page.goto('https://www.youtube.com');
|
|
21
|
+
await page.wait(2);
|
|
22
|
+
const data = await page.evaluate(`
|
|
23
|
+
(async () => {
|
|
24
|
+
const channelId = ${JSON.stringify(channelId)};
|
|
25
|
+
const limit = ${limit};
|
|
26
|
+
const cfg = window.ytcfg?.data_ || {};
|
|
27
|
+
const apiKey = cfg.INNERTUBE_API_KEY;
|
|
28
|
+
const context = cfg.INNERTUBE_CONTEXT;
|
|
29
|
+
if (!apiKey || !context) return {error: 'YouTube config not found'};
|
|
30
|
+
|
|
31
|
+
// Resolve handle to browseId if needed
|
|
32
|
+
let browseId = channelId;
|
|
33
|
+
if (channelId.startsWith('@')) {
|
|
34
|
+
const resolveResp = await fetch('/youtubei/v1/navigation/resolve_url?key=' + apiKey + '&prettyPrint=false', {
|
|
35
|
+
method: 'POST', credentials: 'include',
|
|
36
|
+
headers: {'Content-Type': 'application/json'},
|
|
37
|
+
body: JSON.stringify({context, url: 'https://www.youtube.com/' + channelId})
|
|
38
|
+
});
|
|
39
|
+
if (resolveResp.ok) {
|
|
40
|
+
const resolveData = await resolveResp.json();
|
|
41
|
+
browseId = resolveData.endpoint?.browseEndpoint?.browseId || channelId;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fetch channel data
|
|
46
|
+
const resp = await fetch('/youtubei/v1/browse?key=' + apiKey + '&prettyPrint=false', {
|
|
47
|
+
method: 'POST', credentials: 'include',
|
|
48
|
+
headers: {'Content-Type': 'application/json'},
|
|
49
|
+
body: JSON.stringify({context, browseId})
|
|
50
|
+
});
|
|
51
|
+
if (!resp.ok) return {error: 'Channel API returned HTTP ' + resp.status};
|
|
52
|
+
const data = await resp.json();
|
|
53
|
+
|
|
54
|
+
// Channel metadata
|
|
55
|
+
const metadata = data.metadata?.channelMetadataRenderer || {};
|
|
56
|
+
const header = data.header?.pageHeaderRenderer || data.header?.c4TabbedHeaderRenderer || {};
|
|
57
|
+
|
|
58
|
+
// Subscriber count from header
|
|
59
|
+
let subscriberCount = '';
|
|
60
|
+
try {
|
|
61
|
+
const rows = header.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows || [];
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
for (const part of (row.metadataParts || [])) {
|
|
64
|
+
const text = part.text?.content || '';
|
|
65
|
+
if (text.includes('subscriber')) subscriberCount = text;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
69
|
+
// Fallback for old c4TabbedHeaderRenderer format
|
|
70
|
+
if (!subscriberCount && header.subscriberCountText?.simpleText) {
|
|
71
|
+
subscriberCount = header.subscriberCountText.simpleText;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Extract recent videos from Home tab
|
|
75
|
+
const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
|
|
76
|
+
const homeTab = tabs.find(t => t.tabRenderer?.selected);
|
|
77
|
+
const recentVideos = [];
|
|
78
|
+
|
|
79
|
+
if (homeTab) {
|
|
80
|
+
const sections = homeTab.tabRenderer?.content?.sectionListRenderer?.contents || [];
|
|
81
|
+
for (const section of sections) {
|
|
82
|
+
for (const shelf of (section.itemSectionRenderer?.contents || [])) {
|
|
83
|
+
for (const item of (shelf.shelfRenderer?.content?.horizontalListRenderer?.items || [])) {
|
|
84
|
+
// New lockupViewModel format
|
|
85
|
+
const lvm = item.lockupViewModel;
|
|
86
|
+
if (lvm && lvm.contentType === 'LOCKUP_CONTENT_TYPE_VIDEO' && recentVideos.length < limit) {
|
|
87
|
+
const meta = lvm.metadata?.lockupMetadataViewModel;
|
|
88
|
+
const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
|
|
89
|
+
const viewsAndTime = (rows[0]?.metadataParts || []).map(p => p.text?.content).filter(Boolean).join(' | ');
|
|
90
|
+
let duration = '';
|
|
91
|
+
for (const ov of (lvm.contentImage?.thumbnailViewModel?.overlays || [])) {
|
|
92
|
+
for (const b of (ov.thumbnailBottomOverlayViewModel?.badges || [])) {
|
|
93
|
+
if (b.thumbnailBadgeViewModel?.text) duration = b.thumbnailBadgeViewModel.text;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
recentVideos.push({
|
|
97
|
+
title: meta?.title?.content || '',
|
|
98
|
+
duration,
|
|
99
|
+
views: viewsAndTime,
|
|
100
|
+
url: 'https://www.youtube.com/watch?v=' + lvm.contentId,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Legacy gridVideoRenderer format
|
|
104
|
+
if (item.gridVideoRenderer && recentVideos.length < limit) {
|
|
105
|
+
const v = item.gridVideoRenderer;
|
|
106
|
+
recentVideos.push({
|
|
107
|
+
title: v.title?.runs?.[0]?.text || v.title?.simpleText || '',
|
|
108
|
+
duration: v.thumbnailOverlays?.[0]?.thumbnailOverlayTimeStatusRenderer?.text?.simpleText || '',
|
|
109
|
+
views: (v.shortViewCountText?.simpleText || '') + (v.publishedTimeText?.simpleText ? ' | ' + v.publishedTimeText.simpleText : ''),
|
|
110
|
+
url: 'https://www.youtube.com/watch?v=' + v.videoId,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
name: metadata.title || '',
|
|
120
|
+
channelId: metadata.externalId || browseId,
|
|
121
|
+
handle: metadata.vanityChannelUrl?.split('/').pop() || '',
|
|
122
|
+
description: (metadata.description || '').substring(0, 500),
|
|
123
|
+
subscribers: subscriberCount,
|
|
124
|
+
url: metadata.channelUrl || 'https://www.youtube.com/channel/' + browseId,
|
|
125
|
+
keywords: metadata.keywords || '',
|
|
126
|
+
recentVideos,
|
|
127
|
+
};
|
|
128
|
+
})()
|
|
129
|
+
`);
|
|
130
|
+
if (!data || typeof data !== 'object')
|
|
131
|
+
throw new CommandExecutionError('Failed to fetch channel data');
|
|
132
|
+
if (data.error)
|
|
133
|
+
throw new CommandExecutionError(String(data.error));
|
|
134
|
+
const result = data;
|
|
135
|
+
const videos = result.recentVideos;
|
|
136
|
+
delete result.recentVideos;
|
|
137
|
+
// Channel info as field/value pairs + recent videos as table
|
|
138
|
+
const rows = Object.entries(result).map(([field, value]) => ({
|
|
139
|
+
field,
|
|
140
|
+
value: String(value),
|
|
141
|
+
}));
|
|
142
|
+
if (videos && videos.length > 0) {
|
|
143
|
+
rows.push({ field: '---', value: '--- Recent Videos ---' });
|
|
144
|
+
for (const v of videos) {
|
|
145
|
+
rows.push({ field: v.title, value: `${v.duration} | ${v.views} | ${v.url}` });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return rows;
|
|
149
|
+
},
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube comments — get video comments via InnerTube API.
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
6
|
+
import { parseVideoId } from './utils.js';
|
|
7
|
+
cli({
|
|
8
|
+
site: 'youtube',
|
|
9
|
+
name: 'comments',
|
|
10
|
+
description: 'Get YouTube video comments',
|
|
11
|
+
domain: 'www.youtube.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' },
|
|
15
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max comments (max 100)' },
|
|
16
|
+
],
|
|
17
|
+
columns: ['rank', 'author', 'text', 'likes', 'replies', 'time'],
|
|
18
|
+
func: async (page, kwargs) => {
|
|
19
|
+
const videoId = parseVideoId(kwargs.url);
|
|
20
|
+
const limit = Math.min(kwargs.limit || 20, 100);
|
|
21
|
+
await page.goto(`https://www.youtube.com/watch?v=${videoId}`);
|
|
22
|
+
await page.wait(3);
|
|
23
|
+
const data = await page.evaluate(`
|
|
24
|
+
(async () => {
|
|
25
|
+
const videoId = ${JSON.stringify(videoId)};
|
|
26
|
+
const limit = ${limit};
|
|
27
|
+
const cfg = window.ytcfg?.data_ || {};
|
|
28
|
+
const apiKey = cfg.INNERTUBE_API_KEY;
|
|
29
|
+
const context = cfg.INNERTUBE_CONTEXT;
|
|
30
|
+
if (!apiKey || !context) return {error: 'YouTube config not found'};
|
|
31
|
+
|
|
32
|
+
// Step 1: Get comment continuation token
|
|
33
|
+
let continuationToken = null;
|
|
34
|
+
|
|
35
|
+
// Try from current page ytInitialData
|
|
36
|
+
if (window.ytInitialData) {
|
|
37
|
+
const results = window.ytInitialData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
|
|
38
|
+
const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');
|
|
39
|
+
continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fallback: fetch via next API
|
|
43
|
+
if (!continuationToken) {
|
|
44
|
+
const nextResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {
|
|
45
|
+
method: 'POST', credentials: 'include',
|
|
46
|
+
headers: {'Content-Type': 'application/json'},
|
|
47
|
+
body: JSON.stringify({context, videoId})
|
|
48
|
+
});
|
|
49
|
+
if (!nextResp.ok) return {error: 'Failed to get video data: HTTP ' + nextResp.status};
|
|
50
|
+
const nextData = await nextResp.json();
|
|
51
|
+
const results = nextData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
|
|
52
|
+
const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');
|
|
53
|
+
continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!continuationToken) return {error: 'No comment section found — comments may be disabled'};
|
|
57
|
+
|
|
58
|
+
// Step 2: Fetch comments
|
|
59
|
+
const commentResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {
|
|
60
|
+
method: 'POST', credentials: 'include',
|
|
61
|
+
headers: {'Content-Type': 'application/json'},
|
|
62
|
+
body: JSON.stringify({context, continuation: continuationToken})
|
|
63
|
+
});
|
|
64
|
+
if (!commentResp.ok) return {error: 'Failed to fetch comments: HTTP ' + commentResp.status};
|
|
65
|
+
const commentData = await commentResp.json();
|
|
66
|
+
|
|
67
|
+
// Parse from frameworkUpdates (new ViewModel format)
|
|
68
|
+
const mutations = commentData.frameworkUpdates?.entityBatchUpdate?.mutations || [];
|
|
69
|
+
const commentEntities = mutations.filter(m => m.payload?.commentEntityPayload);
|
|
70
|
+
|
|
71
|
+
return commentEntities.slice(0, limit).map((m, i) => {
|
|
72
|
+
const p = m.payload.commentEntityPayload;
|
|
73
|
+
const props = p.properties || {};
|
|
74
|
+
const author = p.author || {};
|
|
75
|
+
const toolbar = p.toolbar || {};
|
|
76
|
+
return {
|
|
77
|
+
rank: i + 1,
|
|
78
|
+
author: author.displayName || '',
|
|
79
|
+
text: (props.content?.content || '').substring(0, 300),
|
|
80
|
+
likes: toolbar.likeCountNotliked || '0',
|
|
81
|
+
replies: toolbar.replyCount || '0',
|
|
82
|
+
time: props.publishedTime || '',
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
})()
|
|
86
|
+
`);
|
|
87
|
+
if (!Array.isArray(data)) {
|
|
88
|
+
const errMsg = data && typeof data === 'object' ? String(data.error || '') : '';
|
|
89
|
+
if (errMsg)
|
|
90
|
+
throw new CommandExecutionError(errMsg);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
return data;
|
|
94
|
+
},
|
|
95
|
+
});
|
package/dist/external-clis.yaml
CHANGED
|
@@ -14,14 +14,6 @@
|
|
|
14
14
|
install:
|
|
15
15
|
mac: "brew install --cask obsidian"
|
|
16
16
|
|
|
17
|
-
- name: readwise
|
|
18
|
-
binary: readwise
|
|
19
|
-
description: "Readwise & Reader CLI — highlights, annotations, reading list"
|
|
20
|
-
homepage: "https://github.com/readwiseio/readwise-cli"
|
|
21
|
-
tags: [reading, highlights]
|
|
22
|
-
install:
|
|
23
|
-
default: "npm install -g @readwiseio/readwise-cli"
|
|
24
|
-
|
|
25
17
|
- name: docker
|
|
26
18
|
binary: docker
|
|
27
19
|
description: "Docker command-line interface"
|
|
@@ -29,12 +21,3 @@
|
|
|
29
21
|
tags: [docker, containers, devops]
|
|
30
22
|
install:
|
|
31
23
|
mac: "brew install --cask docker"
|
|
32
|
-
|
|
33
|
-
- name: gws
|
|
34
|
-
binary: gws
|
|
35
|
-
description: "Google Workspace CLI — Docs, Sheets, Drive, Gmail, Calendar"
|
|
36
|
-
homepage: "https://github.com/nicholasgasior/gws"
|
|
37
|
-
tags: [google, docs, sheets, drive, workspace]
|
|
38
|
-
install:
|
|
39
|
-
mac: "brew install gws"
|
|
40
|
-
default: "npm install -g @nicholasgasior/gws"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './clis/weread/search.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from './registry.js';
|
|
3
|
+
import './clis/weread/search.js';
|
|
4
|
+
describe('weread/search regression', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
it('uses the query argument for the search API and returns urls', async () => {
|
|
9
|
+
const command = getRegistry().get('weread/search');
|
|
10
|
+
expect(command?.func).toBeTypeOf('function');
|
|
11
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
12
|
+
ok: true,
|
|
13
|
+
json: () => Promise.resolve({
|
|
14
|
+
books: [
|
|
15
|
+
{
|
|
16
|
+
bookInfo: {
|
|
17
|
+
title: 'Deep Work',
|
|
18
|
+
author: 'Cal Newport',
|
|
19
|
+
bookId: 'abc123',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
26
|
+
const result = await command.func(null, { query: 'deep work', limit: 5 });
|
|
27
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
28
|
+
expect(String(fetchMock.mock.calls[0][0])).toContain('keyword=deep+work');
|
|
29
|
+
expect(result).toEqual([
|
|
30
|
+
{
|
|
31
|
+
rank: 1,
|
|
32
|
+
title: 'Deep Work',
|
|
33
|
+
author: 'Cal Newport',
|
|
34
|
+
bookId: 'abc123',
|
|
35
|
+
url: 'https://weread.qq.com/web/bookDetail/abc123',
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -74,6 +74,16 @@ export default defineConfig({
|
|
|
74
74
|
{ text: 'Sina Blog', link: '/adapters/browser/sinablog' },
|
|
75
75
|
{ text: 'Substack', link: '/adapters/browser/substack' },
|
|
76
76
|
{ text: 'Pixiv', link: '/adapters/browser/pixiv' },
|
|
77
|
+
{ text: 'Douban', link: '/adapters/browser/douban' },
|
|
78
|
+
{ text: 'Doubao', link: '/adapters/browser/doubao' },
|
|
79
|
+
{ text: 'Facebook', link: '/adapters/browser/facebook' },
|
|
80
|
+
{ text: 'Google', link: '/adapters/browser/google' },
|
|
81
|
+
{ text: 'Instagram', link: '/adapters/browser/instagram' },
|
|
82
|
+
{ text: 'JD.com', link: '/adapters/browser/jd' },
|
|
83
|
+
{ text: 'Medium', link: '/adapters/browser/medium' },
|
|
84
|
+
{ text: 'TikTok', link: '/adapters/browser/tiktok' },
|
|
85
|
+
{ text: 'Web (Generic)', link: '/adapters/browser/web' },
|
|
86
|
+
{ text: 'Weixin', link: '/adapters/browser/weixin' },
|
|
77
87
|
],
|
|
78
88
|
},
|
|
79
89
|
{
|
|
@@ -93,6 +103,8 @@ export default defineConfig({
|
|
|
93
103
|
{ text: 'Sina Finance', link: '/adapters/browser/sinafinance' },
|
|
94
104
|
{ text: 'Stack Overflow', link: '/adapters/browser/stackoverflow' },
|
|
95
105
|
{ text: 'Wikipedia', link: '/adapters/browser/wikipedia' },
|
|
106
|
+
{ text: 'Lobsters', link: '/adapters/browser/lobsters' },
|
|
107
|
+
{ text: 'Steam', link: '/adapters/browser/steam' },
|
|
96
108
|
],
|
|
97
109
|
},
|
|
98
110
|
{
|
|
@@ -106,6 +118,7 @@ export default defineConfig({
|
|
|
106
118
|
{ text: 'ChatWise', link: '/adapters/desktop/chatwise' },
|
|
107
119
|
{ text: 'Notion', link: '/adapters/desktop/notion' },
|
|
108
120
|
{ text: 'Discord', link: '/adapters/desktop/discord' },
|
|
121
|
+
{ text: 'Doubao App', link: '/adapters/desktop/doubao-app' },
|
|
109
122
|
],
|
|
110
123
|
},
|
|
111
124
|
],
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Douyin (抖音创作者中心)
|
|
2
|
+
|
|
3
|
+
**Mode**: 🔐 Browser · **Domain**: `creator.douyin.com`
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
| Command | Description |
|
|
8
|
+
|---------|-------------|
|
|
9
|
+
| `opencli douyin profile` | 获取账号信息 |
|
|
10
|
+
| `opencli douyin videos` | 获取作品列表 |
|
|
11
|
+
| `opencli douyin drafts` | 获取草稿列表 |
|
|
12
|
+
| `opencli douyin draft` | 上传视频并保存为草稿 |
|
|
13
|
+
| `opencli douyin publish` | 定时发布视频到抖音 |
|
|
14
|
+
| `opencli douyin update` | 更新视频信息 |
|
|
15
|
+
| `opencli douyin delete` | 删除作品 |
|
|
16
|
+
| `opencli douyin stats` | 查询作品数据分析 |
|
|
17
|
+
| `opencli douyin collections` | 获取合集列表 |
|
|
18
|
+
| `opencli douyin activities` | 获取官方活动列表 |
|
|
19
|
+
| `opencli douyin location` | 搜索发布可用的地理位置 |
|
|
20
|
+
| `opencli douyin hashtag search` | 按关键词搜索话题 |
|
|
21
|
+
| `opencli douyin hashtag suggest` | 基于封面 URI 推荐话题 |
|
|
22
|
+
| `opencli douyin hashtag hot` | 获取热点词 |
|
|
23
|
+
|
|
24
|
+
## Usage Examples
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 账号与作品
|
|
28
|
+
opencli douyin profile
|
|
29
|
+
opencli douyin videos --limit 10
|
|
30
|
+
opencli douyin videos --status scheduled
|
|
31
|
+
opencli douyin drafts
|
|
32
|
+
|
|
33
|
+
# 发布前辅助信息
|
|
34
|
+
opencli douyin collections
|
|
35
|
+
opencli douyin activities
|
|
36
|
+
opencli douyin location "东京塔"
|
|
37
|
+
opencli douyin hashtag search "春游"
|
|
38
|
+
opencli douyin hashtag hot --limit 10
|
|
39
|
+
|
|
40
|
+
# 保存草稿
|
|
41
|
+
opencli douyin draft ./video.mp4 \
|
|
42
|
+
--title "春游 vlog" \
|
|
43
|
+
--caption "#春游 先存草稿"
|
|
44
|
+
|
|
45
|
+
# 定时发布
|
|
46
|
+
opencli douyin publish ./video.mp4 \
|
|
47
|
+
--title "春游 vlog" \
|
|
48
|
+
--caption "#春游 今天去看樱花" \
|
|
49
|
+
--schedule "2026-04-08T12:00:00+09:00"
|
|
50
|
+
|
|
51
|
+
# 也支持 Unix 秒字符串
|
|
52
|
+
opencli douyin publish ./video.mp4 \
|
|
53
|
+
--title "春游 vlog" \
|
|
54
|
+
--schedule 1775617200
|
|
55
|
+
|
|
56
|
+
# 更新与删除
|
|
57
|
+
opencli douyin update 1234567890 --caption "更新后的文案"
|
|
58
|
+
opencli douyin update 1234567890 --reschedule "2026-04-09T20:00:00+09:00"
|
|
59
|
+
opencli douyin delete 1234567890
|
|
60
|
+
|
|
61
|
+
# JSON 输出
|
|
62
|
+
opencli douyin profile -f json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Prerequisites
|
|
66
|
+
|
|
67
|
+
- Chrome running and **logged into** `creator.douyin.com`
|
|
68
|
+
- The logged-in account must have access to Douyin Creator Center publishing features
|
|
69
|
+
- [Browser Bridge extension](/guide/browser-bridge) installed
|
|
70
|
+
|
|
71
|
+
## Notes
|
|
72
|
+
|
|
73
|
+
- `publish` requires `--schedule` to be at least 2 hours later and no more than 14 days later
|
|
74
|
+
- `draft` and `publish` upload the video through Douyin/ByteDance browser-authenticated APIs, so cookies in the active browser session must be valid
|
|
75
|
+
- `hashtag suggest` expects a valid `cover`/`cover_uri` value produced during the publish pipeline; for normal manual use, `hashtag search` and `hashtag hot` are usually more convenient
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
# Quick start
|
|
38
38
|
opencli twitter trending --limit 5
|
|
39
39
|
|
|
40
|
+
# Search top tweets (default)
|
|
41
|
+
opencli twitter search "react 19"
|
|
42
|
+
|
|
43
|
+
# Search latest/live tweets
|
|
44
|
+
opencli twitter search "react 19" --filter live
|
|
45
|
+
|
|
40
46
|
# JSON output
|
|
41
47
|
opencli twitter trending -f json
|
|
42
48
|
|
package/docs/adapters/index.md
CHANGED
|
@@ -11,7 +11,7 @@ Run `opencli list` for the live registry.
|
|
|
11
11
|
| **[bilibili](/adapters/browser/bilibili)** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` `download` | 🔐 Browser |
|
|
12
12
|
| **[zhihu](/adapters/browser/zhihu)** | `hot` `search` `question` `download` | 🔐 Browser |
|
|
13
13
|
| **[xiaohongshu](/adapters/browser/xiaohongshu)** | `search` `notifications` `feed` `user` `download` `publish` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` | 🔐 Browser |
|
|
14
|
-
| **[xueqiu](/adapters/browser/xueqiu)** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` `earnings-date` | 🔐 Browser |
|
|
14
|
+
| **[xueqiu](/adapters/browser/xueqiu)** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` `earnings-date` `fund-holdings` `fund-snapshot` | 🔐 Browser |
|
|
15
15
|
| **[youtube](/adapters/browser/youtube)** | `search` `video` `transcript` | 🔐 Browser |
|
|
16
16
|
| **[v2ex](/adapters/browser/v2ex)** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 🌐 / 🔐 |
|
|
17
17
|
| **[bloomberg](/adapters/browser/bloomberg)** | `main` `markets` `economics` `industries` `tech` `politics` `businessweek` `opinions` `feeds` `news` | 🌐 / 🔐 |
|
|
@@ -38,6 +38,10 @@ Run `opencli list` for the live registry.
|
|
|
38
38
|
| **[substack](/adapters/browser/substack)** | `feed` `search` `publication` | 🔐 Browser |
|
|
39
39
|
| **[pixiv](/adapters/browser/pixiv)** | `ranking` `search` `user` `illusts` `detail` `download` | 🔐 Browser |
|
|
40
40
|
| **[tiktok](/adapters/browser/tiktok)** | `explore` `search` `profile` `user` `following` `follow` `unfollow` `like` `unlike` `comment` `save` `unsave` `live` `notifications` `friends` | 🔐 Browser |
|
|
41
|
+
| **[google](/adapters/browser/google)** | `news` `search` `suggest` `trends` | 🌐 / 🔐 |
|
|
42
|
+
| **[jd](/adapters/browser/jd)** | `item` | 🔐 Browser |
|
|
43
|
+
| **[web](/adapters/browser/web)** | `read` | 🔐 Browser |
|
|
44
|
+
| **[weixin](/adapters/browser/weixin)** | `download` | 🔐 Browser |
|
|
41
45
|
|
|
42
46
|
## Public API Adapters
|
|
43
47
|
|
|
@@ -57,6 +61,7 @@ Run `opencli list` for the live registry.
|
|
|
57
61
|
| **[stackoverflow](/adapters/browser/stackoverflow)** | `hot` `search` `bounties` `unanswered` | 🌐 Public |
|
|
58
62
|
| **[wikipedia](/adapters/browser/wikipedia)** | `search` `summary` `random` `trending` | 🌐 Public |
|
|
59
63
|
| **[lobsters](/adapters/browser/lobsters)** | `hot` `newest` `active` `tag` | 🌐 Public |
|
|
64
|
+
| **[steam](/adapters/browser/steam)** | `top-sellers` | 🌐 Public |
|
|
60
65
|
|
|
61
66
|
## Desktop Adapters
|
|
62
67
|
|