@jackwener/opencli 1.7.20 → 1.7.22
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/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/cli-manifest.json +233 -72
- package/clis/_shared/search-adapter.js +70 -0
- package/clis/boss/chatlist.js +96 -14
- package/clis/boss/chatlist.test.js +211 -0
- package/clis/boss/chatmsg.js +98 -24
- package/clis/boss/chatmsg.test.js +230 -0
- package/clis/boss/utils.js +257 -12
- package/clis/boss/utils.test.js +34 -0
- package/clis/brave/search.js +80 -0
- package/clis/brave/search.test.js +76 -0
- package/clis/duckduckgo/search.js +131 -0
- package/clis/duckduckgo/search.test.js +128 -0
- package/clis/duckduckgo/suggest.js +45 -0
- package/clis/duckduckgo/suggest.test.js +66 -0
- package/clis/facebook/feed.js +301 -56
- package/clis/facebook/feed.test.js +169 -0
- package/clis/reddit/comment.js +0 -1
- package/clis/reddit/frontpage.js +0 -1
- package/clis/reddit/home.js +0 -1
- package/clis/reddit/popular.js +0 -1
- package/clis/reddit/read.js +0 -1
- package/clis/reddit/read.test.js +2 -2
- package/clis/reddit/save.js +0 -1
- package/clis/reddit/saved.js +0 -1
- package/clis/reddit/search.js +0 -1
- package/clis/reddit/subreddit-info.js +0 -1
- package/clis/reddit/subreddit.js +0 -1
- package/clis/reddit/subscribe.js +0 -1
- package/clis/reddit/upvote.js +0 -1
- package/clis/reddit/upvoted.js +0 -1
- package/clis/reddit/user-comments.js +0 -1
- package/clis/reddit/user-posts.js +0 -1
- package/clis/reddit/user.js +0 -1
- package/clis/reddit/whoami.js +0 -1
- package/clis/rednote/rednote.test.js +65 -0
- package/clis/rednote/search.js +11 -5
- package/clis/twitter/article.js +0 -1
- package/clis/twitter/bookmark-folder.js +0 -1
- package/clis/twitter/bookmark-folders.js +0 -1
- package/clis/twitter/bookmarks.js +0 -1
- package/clis/twitter/download.js +0 -1
- package/clis/twitter/followers.js +0 -1
- package/clis/twitter/following.js +0 -1
- package/clis/twitter/likes.js +0 -1
- package/clis/twitter/list-tweets.js +0 -1
- package/clis/twitter/lists.js +0 -1
- package/clis/twitter/notifications.js +0 -1
- package/clis/twitter/profile.js +0 -1
- package/clis/twitter/search.js +0 -1
- package/clis/twitter/thread.js +0 -1
- package/clis/twitter/timeline.js +0 -1
- package/clis/twitter/trending.js +0 -1
- package/clis/twitter/tweets.js +0 -1
- package/clis/weibo/comments.js +3 -4
- package/clis/weibo/envelope.test.js +85 -0
- package/clis/weibo/favorites.js +4 -4
- package/clis/weibo/feed.js +3 -5
- package/clis/weibo/hot.js +3 -4
- package/clis/weibo/me.js +3 -5
- package/clis/weibo/post.js +3 -4
- package/clis/weibo/search.js +4 -3
- package/clis/weibo/user.js +3 -4
- package/clis/weibo/utils.js +34 -5
- package/clis/weibo/utils.test.js +36 -0
- package/clis/xiaohongshu/search.js +34 -16
- package/clis/xiaohongshu/search.test.js +66 -11
- package/clis/yahoo/search.js +92 -0
- package/clis/yahoo/search.test.js +94 -0
- package/dist/src/cli.js +1 -1
- package/dist/src/external-clis.yaml +12 -0
- package/dist/src/external.d.ts +6 -1
- package/dist/src/external.test.js +19 -0
- package/package.json +1 -1
|
@@ -7,7 +7,6 @@ cli({
|
|
|
7
7
|
domain: 'reddit.com',
|
|
8
8
|
strategy: Strategy.COOKIE,
|
|
9
9
|
browser: true,
|
|
10
|
-
siteSession: 'persistent',
|
|
11
10
|
args: [
|
|
12
11
|
{ name: 'username', type: 'string', required: true, positional: true, help: 'Reddit username (no `u/` prefix needed)' },
|
|
13
12
|
{ name: 'limit', type: 'int', default: 15 },
|
|
@@ -7,7 +7,6 @@ cli({
|
|
|
7
7
|
domain: 'reddit.com',
|
|
8
8
|
strategy: Strategy.COOKIE,
|
|
9
9
|
browser: true,
|
|
10
|
-
siteSession: 'persistent',
|
|
11
10
|
args: [
|
|
12
11
|
{ name: 'username', type: 'string', required: true, positional: true, help: 'Reddit username (no `u/` prefix needed)' },
|
|
13
12
|
{ name: 'limit', type: 'int', default: 15 },
|
package/clis/reddit/user.js
CHANGED
package/clis/reddit/whoami.js
CHANGED
|
@@ -31,6 +31,14 @@ function createPageMock(evaluateResult) {
|
|
|
31
31
|
getCookies: vi.fn().mockResolvedValue([{ name: 'sid', value: 'secret', domain: 'www.rednote.com' }]),
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
function createSearchPageMock(evaluateResults) {
|
|
35
|
+
const page = createPageMock(undefined);
|
|
36
|
+
page.evaluate = vi.fn();
|
|
37
|
+
for (const result of evaluateResults) {
|
|
38
|
+
page.evaluate.mockResolvedValueOnce(result);
|
|
39
|
+
}
|
|
40
|
+
return page;
|
|
41
|
+
}
|
|
34
42
|
|
|
35
43
|
describe('rednote note URL identity', () => {
|
|
36
44
|
const download = getRegistry().get('rednote/download');
|
|
@@ -130,6 +138,63 @@ describe('rednote argument validation', () => {
|
|
|
130
138
|
});
|
|
131
139
|
});
|
|
132
140
|
|
|
141
|
+
describe('rednote search browser-bridge envelopes', () => {
|
|
142
|
+
const search = getRegistry().get('rednote/search');
|
|
143
|
+
|
|
144
|
+
it('unwraps login-wall wait result envelopes before auth handling', async () => {
|
|
145
|
+
const page = createSearchPageMock([
|
|
146
|
+
{ session: 'site:rednote', data: 'login_wall' },
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
await expect(search.func(page, { query: 'tesla', limit: 5 })).rejects.toMatchObject({
|
|
150
|
+
code: 'AUTH_REQUIRED',
|
|
151
|
+
message: expect.stringContaining('blocked behind a login wall'),
|
|
152
|
+
});
|
|
153
|
+
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('unwraps search extraction envelopes and preserves rednote row shape', async () => {
|
|
157
|
+
const url = 'https://www.rednote.com/search_result/68e90be80000000004022e66?xsec_token=test-token';
|
|
158
|
+
const page = createSearchPageMock([
|
|
159
|
+
'content',
|
|
160
|
+
1,
|
|
161
|
+
{
|
|
162
|
+
session: 'site:rednote',
|
|
163
|
+
data: [{
|
|
164
|
+
title: 'rednote result',
|
|
165
|
+
author: 'author',
|
|
166
|
+
likes: '12',
|
|
167
|
+
url,
|
|
168
|
+
author_url: 'https://www.rednote.com/user/profile/u1',
|
|
169
|
+
}],
|
|
170
|
+
},
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
await expect(search.func(page, { query: 'tesla', limit: 1 })).resolves.toEqual([{
|
|
174
|
+
rank: 1,
|
|
175
|
+
title: 'rednote result',
|
|
176
|
+
author: 'author',
|
|
177
|
+
likes: '12',
|
|
178
|
+
published_at: '2025-10-10',
|
|
179
|
+
url,
|
|
180
|
+
author_url: 'https://www.rednote.com/user/profile/u1',
|
|
181
|
+
}]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('fails typed instead of silently returning [] for malformed extraction payloads', async () => {
|
|
185
|
+
const page = createSearchPageMock([
|
|
186
|
+
'content',
|
|
187
|
+
1,
|
|
188
|
+
{ session: 'site:rednote', data: { rows: [] } },
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
await expect(search.func(page, { query: 'tesla', limit: 1 })).rejects.toMatchObject({
|
|
192
|
+
code: 'COMMAND_EXEC',
|
|
193
|
+
message: expect.stringContaining('payload shape'),
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
133
198
|
describe('rednote Pinia store failures', () => {
|
|
134
199
|
it('maps feed store read failure to CommandExecutionError', async () => {
|
|
135
200
|
const command = getRegistry().get('rednote/feed');
|
package/clis/rednote/search.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* 1:1 comparison between the two frontends.
|
|
7
7
|
*/
|
|
8
8
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
9
|
-
import { ArgumentError, AuthRequiredError } from '@jackwener/opencli/errors';
|
|
10
|
-
import { buildScrollUntilJs, buildSearchExtractJs, noteIdToDate } from '../xiaohongshu/search.js';
|
|
9
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
10
|
+
import { buildScrollUntilJs, buildSearchExtractJs, noteIdToDate, unwrapEvaluateResult } from '../xiaohongshu/search.js';
|
|
11
11
|
|
|
12
12
|
function parseLimit(raw) {
|
|
13
13
|
const parsed = Number(raw);
|
|
@@ -19,6 +19,13 @@ function parseLimit(raw) {
|
|
|
19
19
|
}
|
|
20
20
|
return parsed;
|
|
21
21
|
}
|
|
22
|
+
function requireSearchRows(payload) {
|
|
23
|
+
const rows = unwrapEvaluateResult(payload);
|
|
24
|
+
if (!Array.isArray(rows)) {
|
|
25
|
+
throw new CommandExecutionError('Unexpected Rednote search extraction payload shape; expected an array of rows.');
|
|
26
|
+
}
|
|
27
|
+
return rows;
|
|
28
|
+
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
* Wait for search results or login wall using MutationObserver (max 5s).
|
|
@@ -78,7 +85,7 @@ cli({
|
|
|
78
85
|
const limit = parseLimit(kwargs.limit ?? 20);
|
|
79
86
|
const keyword = encodeURIComponent(kwargs.query);
|
|
80
87
|
await page.goto(`https://www.rednote.com/search_result?keyword=${keyword}&source=web_search_result_notes`);
|
|
81
|
-
const waitResult = await page.evaluate(WAIT_FOR_CONTENT_JS);
|
|
88
|
+
const waitResult = unwrapEvaluateResult(await page.evaluate(WAIT_FOR_CONTENT_JS));
|
|
82
89
|
if (waitResult === 'login_wall') {
|
|
83
90
|
throw new AuthRequiredError('www.rednote.com', 'Rednote search results are blocked behind a login wall');
|
|
84
91
|
}
|
|
@@ -87,8 +94,7 @@ cli({
|
|
|
87
94
|
// `autoScroll({ times: 2 })` capped extraction at ~13 notes regardless
|
|
88
95
|
// of `--limit`.
|
|
89
96
|
await page.evaluate(buildScrollUntilJs(limit));
|
|
90
|
-
const
|
|
91
|
-
const data = Array.isArray(payload) ? payload : [];
|
|
97
|
+
const data = requireSearchRows(await page.evaluate(buildSearchExtractJs('www.rednote.com')));
|
|
92
98
|
return data
|
|
93
99
|
.filter((item) => item.title)
|
|
94
100
|
.slice(0, limit)
|
package/clis/twitter/article.js
CHANGED
|
@@ -124,7 +124,6 @@ cli({
|
|
|
124
124
|
domain: 'x.com',
|
|
125
125
|
strategy: Strategy.COOKIE,
|
|
126
126
|
browser: true,
|
|
127
|
-
siteSession: 'persistent',
|
|
128
127
|
args: [
|
|
129
128
|
{ name: 'folder-id', positional: true, type: 'string', required: true, help: 'Folder id from `opencli twitter bookmark-folders`.' },
|
|
130
129
|
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of bookmarks to return (default 20).' },
|
|
@@ -108,7 +108,6 @@ cli({
|
|
|
108
108
|
domain: 'x.com',
|
|
109
109
|
strategy: Strategy.COOKIE,
|
|
110
110
|
browser: true,
|
|
111
|
-
siteSession: 'persistent',
|
|
112
111
|
args: [
|
|
113
112
|
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of bookmarks to return (default 20).' },
|
|
114
113
|
{ name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the bookmarks by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API\'s native (saved-time) ordering.' },
|
package/clis/twitter/download.js
CHANGED
|
@@ -15,7 +15,6 @@ cli({
|
|
|
15
15
|
description: 'Download Twitter/X media (images and videos). Provide either <username> to scan a profile\'s media tab, or --tweet-url to download a single tweet.',
|
|
16
16
|
domain: 'x.com',
|
|
17
17
|
strategy: Strategy.COOKIE,
|
|
18
|
-
siteSession: 'persistent',
|
|
19
18
|
args: [
|
|
20
19
|
{ name: 'username', positional: true, help: 'Twitter username (with or without @) to scan their /media tab. Either <username> or --tweet-url is required.' },
|
|
21
20
|
{ name: 'tweet-url', help: 'Single tweet URL to download. Use this OR <username>, not both required at once.' },
|
package/clis/twitter/likes.js
CHANGED
|
@@ -143,7 +143,6 @@ cli({
|
|
|
143
143
|
domain: 'x.com',
|
|
144
144
|
strategy: Strategy.COOKIE,
|
|
145
145
|
browser: true,
|
|
146
|
-
siteSession: 'persistent',
|
|
147
146
|
args: [
|
|
148
147
|
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (with or without @). Defaults to the logged-in user when omitted.' },
|
|
149
148
|
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of liked tweets to return (default 20).' },
|
|
@@ -115,7 +115,6 @@ cli({
|
|
|
115
115
|
domain: 'x.com',
|
|
116
116
|
strategy: Strategy.COOKIE,
|
|
117
117
|
browser: true,
|
|
118
|
-
siteSession: 'persistent',
|
|
119
118
|
args: [
|
|
120
119
|
{ name: 'listId', positional: true, type: 'string', required: true, help: 'Numeric ID of a Twitter/X list (e.g. from `opencli twitter lists`)' },
|
|
121
120
|
{ name: 'limit', type: 'int', default: 50 },
|
package/clis/twitter/lists.js
CHANGED
package/clis/twitter/profile.js
CHANGED
|
@@ -11,7 +11,6 @@ cli({
|
|
|
11
11
|
domain: 'x.com',
|
|
12
12
|
strategy: Strategy.COOKIE,
|
|
13
13
|
browser: true,
|
|
14
|
-
siteSession: 'persistent',
|
|
15
14
|
args: [
|
|
16
15
|
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (with or without @). Defaults to the logged-in user when omitted.' },
|
|
17
16
|
],
|
package/clis/twitter/search.js
CHANGED
|
@@ -261,7 +261,6 @@ cli({
|
|
|
261
261
|
domain: 'x.com',
|
|
262
262
|
strategy: Strategy.COOKIE,
|
|
263
263
|
browser: true,
|
|
264
|
-
siteSession: 'persistent',
|
|
265
264
|
args: [
|
|
266
265
|
{ name: 'query', type: 'string', required: true, positional: true, help: 'Search query. Raw X operators (e.g. "exact phrase", #tag, OR, lang:en, since:YYYY-MM-DD, from:, since:) are passed through unchanged.' },
|
|
267
266
|
{ name: 'filter', type: 'string', default: 'top', choices: ['top', 'live'], help: 'Legacy alias for --product. Kept for backwards compatibility; if --product is set it wins.' },
|
package/clis/twitter/thread.js
CHANGED
|
@@ -100,7 +100,6 @@ cli({
|
|
|
100
100
|
domain: 'x.com',
|
|
101
101
|
strategy: Strategy.COOKIE,
|
|
102
102
|
browser: true,
|
|
103
|
-
siteSession: 'persistent',
|
|
104
103
|
args: [
|
|
105
104
|
{ name: 'tweet-id', positional: true, type: 'string', required: true, help: 'Tweet numeric ID (e.g. 1234567890) or full status URL' },
|
|
106
105
|
{ name: 'limit', type: 'int', default: 50 },
|
package/clis/twitter/timeline.js
CHANGED
package/clis/twitter/trending.js
CHANGED
package/clis/twitter/tweets.js
CHANGED
|
@@ -221,7 +221,6 @@ cli({
|
|
|
221
221
|
domain: 'x.com',
|
|
222
222
|
strategy: Strategy.COOKIE,
|
|
223
223
|
browser: true,
|
|
224
|
-
siteSession: 'persistent',
|
|
225
224
|
args: [
|
|
226
225
|
{ name: 'username', type: 'string', positional: true, help: 'Twitter screen name (with or without @). Defaults to the logged-in user when omitted.' },
|
|
227
226
|
{ name: 'limit', type: 'int', default: 20, help: 'Max tweets to return' },
|
package/clis/weibo/comments.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Weibo comments — get comments on a post.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
+
import { requireArrayEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
5
6
|
cli({
|
|
6
7
|
site: 'weibo',
|
|
7
8
|
name: 'comments',
|
|
@@ -19,7 +20,7 @@ cli({
|
|
|
19
20
|
await page.goto('https://weibo.com');
|
|
20
21
|
await page.wait(2);
|
|
21
22
|
const id = String(kwargs.id);
|
|
22
|
-
const data = await page.evaluate(`
|
|
23
|
+
const data = requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
23
24
|
(async () => {
|
|
24
25
|
const id = ${JSON.stringify(id)};
|
|
25
26
|
const count = ${count};
|
|
@@ -46,9 +47,7 @@ cli({
|
|
|
46
47
|
return item;
|
|
47
48
|
});
|
|
48
49
|
})()
|
|
49
|
-
`);
|
|
50
|
-
if (!Array.isArray(data))
|
|
51
|
-
return [];
|
|
50
|
+
`)), 'weibo comments');
|
|
52
51
|
return data;
|
|
53
52
|
},
|
|
54
53
|
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import './comments.js';
|
|
5
|
+
import './favorites.js';
|
|
6
|
+
import './feed.js';
|
|
7
|
+
import './hot.js';
|
|
8
|
+
import './me.js';
|
|
9
|
+
import './post.js';
|
|
10
|
+
import './search.js';
|
|
11
|
+
import './user.js';
|
|
12
|
+
|
|
13
|
+
function envelope(data) {
|
|
14
|
+
return { session: 'site:weibo:test', data };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makePage(evaluateResults = []) {
|
|
18
|
+
const queue = [...evaluateResults];
|
|
19
|
+
return {
|
|
20
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
evaluate: vi.fn(async (script) => {
|
|
23
|
+
if (String(script).includes('window.scrollBy')) return undefined;
|
|
24
|
+
return queue.length ? queue.shift() : undefined;
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('weibo read adapters Browser Bridge envelopes', () => {
|
|
30
|
+
it('unwraps comments, feed, hot, search, and favorites array payloads', async () => {
|
|
31
|
+
await expect(getRegistry().get('weibo/comments').func(
|
|
32
|
+
makePage([envelope([{ rank: 1, author: 'a', text: 't', likes: 0, replies: 0, time: '' }])]),
|
|
33
|
+
{ id: '123', limit: 1 },
|
|
34
|
+
)).resolves.toHaveLength(1);
|
|
35
|
+
|
|
36
|
+
await expect(getRegistry().get('weibo/feed').func(
|
|
37
|
+
makePage([envelope('123456'), envelope([{ id: 'm1', author: 'a', text: 't', reposts: 0, comments: 0, likes: 0, time: '', url: 'https://weibo.com/1/m1' }])]),
|
|
38
|
+
{ type: 'for-you', limit: 1 },
|
|
39
|
+
)).resolves.toHaveLength(1);
|
|
40
|
+
|
|
41
|
+
await expect(getRegistry().get('weibo/hot').func(
|
|
42
|
+
makePage([envelope([{ rank: 1, word: 'opencli', hot_value: 1, category: '', label: '', url: 'https://s.weibo.com/weibo?q=opencli' }])]),
|
|
43
|
+
{ limit: 1 },
|
|
44
|
+
)).resolves.toHaveLength(1);
|
|
45
|
+
|
|
46
|
+
await expect(getRegistry().get('weibo/search').func(
|
|
47
|
+
makePage([envelope([{ id: 'm1', title: 'OpenCLI', author: 'a', time: '', url: 'https://weibo.com/1/m1' }])]),
|
|
48
|
+
{ keyword: 'opencli', limit: 1 },
|
|
49
|
+
)).resolves.toEqual([{ rank: 1, id: 'm1', title: 'OpenCLI', author: 'a', time: '', url: 'https://weibo.com/1/m1' }]);
|
|
50
|
+
|
|
51
|
+
await expect(getRegistry().get('weibo/favorites').func(
|
|
52
|
+
makePage([envelope('123456'), envelope([{ text: '作者A\n这是一条收藏微博', url: 'https://weibo.com/123/AbCd1' }])]),
|
|
53
|
+
{ limit: 1 },
|
|
54
|
+
)).resolves.toHaveLength(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('unwraps me, post, and user object payloads', async () => {
|
|
58
|
+
await expect(getRegistry().get('weibo/me').func(
|
|
59
|
+
makePage([envelope('123456'), envelope({ screen_name: 'me', uid: '123456' })]),
|
|
60
|
+
{},
|
|
61
|
+
)).resolves.toMatchObject({ screen_name: 'me', uid: '123456' });
|
|
62
|
+
|
|
63
|
+
await expect(getRegistry().get('weibo/post').func(
|
|
64
|
+
makePage([envelope({ id: '1', text: 'post' })]),
|
|
65
|
+
{ id: '1' },
|
|
66
|
+
)).resolves.toContainEqual({ field: 'text', value: 'post' });
|
|
67
|
+
|
|
68
|
+
await expect(getRegistry().get('weibo/user').func(
|
|
69
|
+
makePage([envelope({ screen_name: 'alice', uid: '42' })]),
|
|
70
|
+
{ id: '42' },
|
|
71
|
+
)).resolves.toMatchObject({ screen_name: 'alice', uid: '42' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('fails typed instead of returning empty rows for malformed post-unwrap payloads', async () => {
|
|
75
|
+
await expect(getRegistry().get('weibo/hot').func(
|
|
76
|
+
makePage([envelope({ error: 'API error' })]),
|
|
77
|
+
{ limit: 1 },
|
|
78
|
+
)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
79
|
+
|
|
80
|
+
await expect(getRegistry().get('weibo/user').func(
|
|
81
|
+
makePage([envelope([{ screen_name: 'wrong shape' }])]),
|
|
82
|
+
{ id: '42' },
|
|
83
|
+
)).rejects.toBeInstanceOf(CommandExecutionError);
|
|
84
|
+
});
|
|
85
|
+
});
|
package/clis/weibo/favorites.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
2
|
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
-
import { getSelfUid } from './utils.js';
|
|
3
|
+
import { getSelfUid, requireArrayEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
4
4
|
|
|
5
5
|
const DEFAULT_LIMIT = 20;
|
|
6
6
|
const MAX_LIMIT = 50;
|
|
@@ -123,7 +123,7 @@ cli({
|
|
|
123
123
|
await page.wait(1);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const rawData = await page.evaluate(`
|
|
126
|
+
const rawData = requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
127
127
|
(() => {
|
|
128
128
|
const scrollers = document.querySelectorAll('.wbpro-scroller-item, .vue-recycle-scroller__item-view');
|
|
129
129
|
const out = [];
|
|
@@ -145,9 +145,9 @@ cli({
|
|
|
145
145
|
}
|
|
146
146
|
return out;
|
|
147
147
|
})()
|
|
148
|
-
`);
|
|
148
|
+
`)), 'weibo favorites');
|
|
149
149
|
|
|
150
|
-
if (
|
|
150
|
+
if (rawData.length === 0) {
|
|
151
151
|
throw new EmptyResultError('weibo favorites', 'No favorites were visible on the favorites page');
|
|
152
152
|
}
|
|
153
153
|
|
package/clis/weibo/feed.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Weibo feed — for-you or following timeline.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
-
import { getSelfUid } from './utils.js';
|
|
5
|
+
import { getSelfUid, requireArrayEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
6
6
|
const TIMELINE_ENDPOINTS = {
|
|
7
7
|
'for-you': 'unreadfriendstimeline',
|
|
8
8
|
following: 'friendstimeline',
|
|
@@ -31,7 +31,7 @@ cli({
|
|
|
31
31
|
await page.goto('https://weibo.com');
|
|
32
32
|
await page.wait(2);
|
|
33
33
|
const uid = await getSelfUid(page);
|
|
34
|
-
const data = await page.evaluate(`
|
|
34
|
+
const data = requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
35
35
|
(async () => {
|
|
36
36
|
const uid = ${JSON.stringify(uid)};
|
|
37
37
|
const count = ${count};
|
|
@@ -63,9 +63,7 @@ cli({
|
|
|
63
63
|
return item;
|
|
64
64
|
});
|
|
65
65
|
})()
|
|
66
|
-
`);
|
|
67
|
-
if (!Array.isArray(data))
|
|
68
|
-
return [];
|
|
66
|
+
`)), 'weibo feed');
|
|
69
67
|
return data;
|
|
70
68
|
},
|
|
71
69
|
});
|
package/clis/weibo/hot.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Weibo hot search — browser cookie API.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
+
import { requireArrayEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
5
6
|
cli({
|
|
6
7
|
site: 'weibo',
|
|
7
8
|
name: 'hot',
|
|
@@ -16,7 +17,7 @@ cli({
|
|
|
16
17
|
func: async (page, kwargs) => {
|
|
17
18
|
const count = Math.min(kwargs.limit || 30, 50);
|
|
18
19
|
await page.goto('https://weibo.com');
|
|
19
|
-
const data = await page.evaluate(`
|
|
20
|
+
const data = requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
20
21
|
(async () => {
|
|
21
22
|
const resp = await fetch('/ajax/statuses/hot_band', {credentials: 'include'});
|
|
22
23
|
if (!resp.ok) return {error: 'HTTP ' + resp.status};
|
|
@@ -32,9 +33,7 @@ cli({
|
|
|
32
33
|
url: 'https://s.weibo.com/weibo?q=' + encodeURIComponent('#' + item.word + '#')
|
|
33
34
|
}));
|
|
34
35
|
})()
|
|
35
|
-
`);
|
|
36
|
-
if (!Array.isArray(data))
|
|
37
|
-
return [];
|
|
36
|
+
`)), 'weibo hot');
|
|
38
37
|
return data.slice(0, count);
|
|
39
38
|
},
|
|
40
39
|
});
|
package/clis/weibo/me.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
5
|
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
6
|
-
import { getSelfUid } from './utils.js';
|
|
6
|
+
import { getSelfUid, requireObjectEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
7
7
|
cli({
|
|
8
8
|
site: 'weibo',
|
|
9
9
|
name: 'me',
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
await page.goto('https://weibo.com');
|
|
18
18
|
await page.wait(2);
|
|
19
19
|
const uid = await getSelfUid(page);
|
|
20
|
-
const data = await page.evaluate(`
|
|
20
|
+
const data = requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
21
21
|
(async () => {
|
|
22
22
|
const uid = ${JSON.stringify(uid)};
|
|
23
23
|
|
|
@@ -67,9 +67,7 @@ cli({
|
|
|
67
67
|
profile_url: 'https://weibo.com' + (p.profile_url || '/u/' + p.id),
|
|
68
68
|
};
|
|
69
69
|
})()
|
|
70
|
-
`);
|
|
71
|
-
if (!data || typeof data !== 'object')
|
|
72
|
-
throw new CommandExecutionError('Failed to fetch profile');
|
|
70
|
+
`)), 'weibo me');
|
|
73
71
|
if (data.error)
|
|
74
72
|
throw new CommandExecutionError(String(data.error));
|
|
75
73
|
return data;
|
package/clis/weibo/post.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
5
|
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
6
|
+
import { requireObjectEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
6
7
|
cli({
|
|
7
8
|
site: 'weibo',
|
|
8
9
|
name: 'post',
|
|
@@ -18,7 +19,7 @@ cli({
|
|
|
18
19
|
await page.goto('https://weibo.com');
|
|
19
20
|
await page.wait(2);
|
|
20
21
|
const id = String(kwargs.id);
|
|
21
|
-
const data = await page.evaluate(`
|
|
22
|
+
const data = requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
22
23
|
(async () => {
|
|
23
24
|
const id = ${JSON.stringify(id)};
|
|
24
25
|
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
@@ -63,9 +64,7 @@ cli({
|
|
|
63
64
|
|
|
64
65
|
return result;
|
|
65
66
|
})()
|
|
66
|
-
`);
|
|
67
|
-
if (!data || typeof data !== 'object')
|
|
68
|
-
throw new CommandExecutionError('Failed to fetch post');
|
|
67
|
+
`)), 'weibo post');
|
|
69
68
|
if (data.error)
|
|
70
69
|
throw new CommandExecutionError(String(data.error));
|
|
71
70
|
return Object.entries(data).map(([field, value]) => ({
|
package/clis/weibo/search.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
5
|
import { CliError } from '@jackwener/opencli/errors';
|
|
6
|
+
import { requireArrayEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
6
7
|
cli({
|
|
7
8
|
site: 'weibo',
|
|
8
9
|
name: 'search',
|
|
@@ -21,7 +22,7 @@ cli({
|
|
|
21
22
|
const keyword = encodeURIComponent(String(kwargs.keyword ?? '').trim());
|
|
22
23
|
await page.goto(`https://s.weibo.com/weibo?q=${keyword}`);
|
|
23
24
|
await page.wait(2);
|
|
24
|
-
const data = await page.evaluate(`
|
|
25
|
+
const data = requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
25
26
|
(() => {
|
|
26
27
|
const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
27
28
|
const absoluteUrl = (href) => {
|
|
@@ -67,8 +68,8 @@ cli({
|
|
|
67
68
|
|
|
68
69
|
return rows;
|
|
69
70
|
})()
|
|
70
|
-
`);
|
|
71
|
-
if (
|
|
71
|
+
`)), 'weibo search');
|
|
72
|
+
if (data.length === 0) {
|
|
72
73
|
throw new CliError('NOT_FOUND', 'No Weibo search results found', 'Try a different keyword or ensure you are logged into weibo.com');
|
|
73
74
|
}
|
|
74
75
|
return data.slice(0, limit).map((item, index) => ({
|
package/clis/weibo/user.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
5
|
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
6
|
+
import { requireObjectEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
6
7
|
cli({
|
|
7
8
|
site: 'weibo',
|
|
8
9
|
name: 'user',
|
|
@@ -18,7 +19,7 @@ cli({
|
|
|
18
19
|
await page.goto('https://weibo.com');
|
|
19
20
|
await page.wait(2);
|
|
20
21
|
const id = String(kwargs.id);
|
|
21
|
-
const data = await page.evaluate(`
|
|
22
|
+
const data = requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
22
23
|
(async () => {
|
|
23
24
|
const id = ${JSON.stringify(id)};
|
|
24
25
|
const isUid = /^\\d+$/.test(id);
|
|
@@ -54,9 +55,7 @@ cli({
|
|
|
54
55
|
ip_location: d.ip_location || '',
|
|
55
56
|
};
|
|
56
57
|
})()
|
|
57
|
-
`);
|
|
58
|
-
if (!data || typeof data !== 'object')
|
|
59
|
-
throw new CommandExecutionError('Failed to fetch user profile');
|
|
58
|
+
`)), 'weibo user');
|
|
60
59
|
if (data.error)
|
|
61
60
|
throw new CommandExecutionError(String(data.error));
|
|
62
61
|
return data;
|