@jackwener/opencli 1.7.21 → 1.8.0
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 +31 -148
- package/README.zh-CN.md +38 -211
- package/cli-manifest.json +6423 -4260
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/boss/utils.js +17 -1
- package/clis/boss/utils.test.js +34 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +226 -0
- package/clis/suno/generate.test.js +243 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +62 -0
- package/clis/suno/utils.js +540 -0
- package/clis/suno/utils.test.js +223 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +12 -5
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/search.js +6 -2
- package/clis/twitter/search.test.js +6 -0
- package/clis/twitter/shared.js +144 -0
- package/clis/twitter/shared.test.js +429 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/weibo/comments.js +3 -4
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- 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/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/search.js +4 -3
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weibo/user.js +3 -4
- package/clis/weibo/utils.js +34 -5
- package/clis/weibo/utils.test.js +36 -0
- package/clis/weread/search-regression.test.js +18 -11
- package/clis/weread/search.js +15 -7
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +2 -1
- package/clis/xiaohongshu/creator-note-detail.test.js +11 -0
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +2 -1
- package/clis/xiaohongshu/creator-notes.test.js +12 -0
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +299 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +12 -0
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +15 -2
- package/clis/zhihu/collection.test.js +46 -0
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -9
- package/clis/zhihu/question.test.js +111 -9
- package/clis/zhihu/search.js +206 -43
- package/clis/zhihu/search.test.js +198 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/cli.js +1 -1
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +110 -0
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +20 -0
- package/dist/src/external.d.ts +6 -1
- package/dist/src/external.test.js +19 -0
- package/dist/src/main.js +14 -2
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
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/publish.js
CHANGED
|
@@ -30,8 +30,15 @@ const SUBMIT_POLL_MS = 500;
|
|
|
30
30
|
const SUBMIT_TIMEOUT_MS = 20_000;
|
|
31
31
|
const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
|
|
32
32
|
|
|
33
|
-
// Weibo PC UI selectors
|
|
34
|
-
|
|
33
|
+
// Weibo PC UI selectors. The CSS-module hash drifts on every frontend
|
|
34
|
+
// rebuild (#1602), so match on the stable placeholder text and keep the
|
|
35
|
+
// legacy hash as a last-resort fallback. Callers pick the LAST visible
|
|
36
|
+
// match because the compose modal renders on top of the home-feed strip.
|
|
37
|
+
const TEXTAREA_SELECTORS = [
|
|
38
|
+
'textarea[placeholder*="有什么新鲜事"]',
|
|
39
|
+
'textarea[placeholder*="新鲜事"]',
|
|
40
|
+
'textarea._input_13iqr_8',
|
|
41
|
+
];
|
|
35
42
|
const FILE_INPUT_SELECTOR = 'input[type="file"][class*="_file_"]';
|
|
36
43
|
|
|
37
44
|
function validateText(text) {
|
|
@@ -125,12 +132,19 @@ cli({
|
|
|
125
132
|
let editorFound = false;
|
|
126
133
|
for (let i = 0; i < Math.ceil(COMPOSE_TIMEOUT_MS / COMPOSE_POLL_MS); i++) {
|
|
127
134
|
const result = await page.evaluate(`
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
(selectors => {
|
|
136
|
+
// Pick the LAST visible match across all selectors so
|
|
137
|
+
// the modal (rendered on top of the home-feed strip)
|
|
138
|
+
// wins over earlier matches. See TEXTAREA_SELECTORS.
|
|
139
|
+
let last = null;
|
|
140
|
+
for (const sel of selectors) {
|
|
141
|
+
for (const t of document.querySelectorAll(sel)) {
|
|
142
|
+
if (t.offsetParent !== null) last = t;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!last) return { found: false };
|
|
146
|
+
return { found: true, visible: true, rectTop: last.getBoundingClientRect().top };
|
|
147
|
+
})(${JSON.stringify(TEXTAREA_SELECTORS)})
|
|
134
148
|
`);
|
|
135
149
|
if (result?.found && result.visible && result.rectTop >= 0) {
|
|
136
150
|
editorFound = true;
|
|
@@ -187,9 +201,14 @@ cli({
|
|
|
187
201
|
// IMPORTANT: Using nativeSetter preserves the textarea's reactive/internal state.
|
|
188
202
|
// Direct ta.value= assignment bypasses Weibo's Vue reactivity and causes "undefined" content.
|
|
189
203
|
const insertResult = await page.evaluateWithArgs(`
|
|
190
|
-
(() => {
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
((selectors) => {
|
|
205
|
+
let ta = null;
|
|
206
|
+
for (const sel of selectors) {
|
|
207
|
+
for (const t of document.querySelectorAll(sel)) {
|
|
208
|
+
if (t.offsetParent !== null) ta = t;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!ta) return { ok: false, message: 'textarea not visible' };
|
|
193
212
|
ta.focus();
|
|
194
213
|
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
195
214
|
if (nativeSetter) {
|
|
@@ -200,7 +219,7 @@ cli({
|
|
|
200
219
|
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
201
220
|
ta.dispatchEvent(new Event('change', { bubbles: true }));
|
|
202
221
|
return { ok: true, valueLength: ta.value.length };
|
|
203
|
-
})()
|
|
222
|
+
})(${JSON.stringify(TEXTAREA_SELECTORS)})
|
|
204
223
|
`, { textContent: text });
|
|
205
224
|
|
|
206
225
|
if (!insertResult?.ok) {
|
|
@@ -233,10 +252,14 @@ cli({
|
|
|
233
252
|
}
|
|
234
253
|
|
|
235
254
|
// Step 8: Wait for success/failure result
|
|
255
|
+
// Use page.evaluate (not evaluateWithArgs): the IIFE doesn't reference
|
|
256
|
+
// any outer args, and evaluateWithArgs would re-declare its const
|
|
257
|
+
// bindings each loop iteration in the same page context, throwing
|
|
258
|
+
// "Identifier already declared" after the first iteration.
|
|
236
259
|
let finalResult = null;
|
|
237
260
|
for (let i = 0; i < Math.ceil(SUBMIT_TIMEOUT_MS / SUBMIT_POLL_MS); i++) {
|
|
238
261
|
await page.wait({ time: SUBMIT_POLL_MS / 1000 });
|
|
239
|
-
finalResult = await page.
|
|
262
|
+
finalResult = await page.evaluate(`
|
|
240
263
|
(() => {
|
|
241
264
|
const successMarkers = ['发布成功', '已发布', '发送成功'];
|
|
242
265
|
const errorMarkers = ['发布失败', '发送失败', '内容违规', '请稍后再试', '频繁'];
|
|
@@ -257,7 +280,7 @@ cli({
|
|
|
257
280
|
}
|
|
258
281
|
return null;
|
|
259
282
|
})()
|
|
260
|
-
|
|
283
|
+
`);
|
|
261
284
|
if (finalResult !== null) break;
|
|
262
285
|
}
|
|
263
286
|
|
|
@@ -61,10 +61,10 @@ describe('weibo publish command', () => {
|
|
|
61
61
|
{ ok: true },
|
|
62
62
|
{ found: true, visible: true, rectTop: 100 },
|
|
63
63
|
{ ok: true, label: '发送' },
|
|
64
|
+
{ ok: true, message: '发送成功' },
|
|
64
65
|
],
|
|
65
66
|
evaluateWithArgsResults: [
|
|
66
67
|
{ ok: true, valueLength: 5 },
|
|
67
|
-
{ ok: true, message: '发送成功' },
|
|
68
68
|
],
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -72,6 +72,9 @@ describe('weibo publish command', () => {
|
|
|
72
72
|
|
|
73
73
|
expect(result).toEqual([{ status: 'success', message: '发送成功', text: 'hello' }]);
|
|
74
74
|
expect(page.goto).toHaveBeenCalledWith('https://weibo.com', { waitUntil: 'load', settleMs: 2000 });
|
|
75
|
+
expect(page.evaluate.mock.calls[2][0]).toContain('有什么新鲜事');
|
|
76
|
+
expect(page.evaluate.mock.calls[2][0]).toContain('textarea._input_13iqr_8');
|
|
77
|
+
expect(page.evaluateWithArgs.mock.calls[0][0]).toContain('有什么新鲜事');
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
it('uploads up to nine images before publishing', async () => {
|
|
@@ -83,11 +86,11 @@ describe('weibo publish command', () => {
|
|
|
83
86
|
{ found: true, visible: true, rectTop: 100 },
|
|
84
87
|
true,
|
|
85
88
|
{ ok: true, label: '发送' },
|
|
89
|
+
{ ok: true, message: '发送成功' },
|
|
86
90
|
],
|
|
87
91
|
evaluateWithArgsResults: [
|
|
88
92
|
{ ok: true, count: 2 },
|
|
89
93
|
{ ok: true, valueLength: 11 },
|
|
90
|
-
{ ok: true, message: '发送成功' },
|
|
91
94
|
],
|
|
92
95
|
});
|
|
93
96
|
|
|
@@ -149,10 +152,10 @@ describe('weibo publish command', () => {
|
|
|
149
152
|
{ ok: true },
|
|
150
153
|
{ found: true, visible: true, rectTop: 100 },
|
|
151
154
|
{ ok: true, label: '发送' },
|
|
155
|
+
{ ok: false, message: '内容违规' },
|
|
152
156
|
],
|
|
153
157
|
evaluateWithArgsResults: [
|
|
154
158
|
{ ok: true, valueLength: 5 },
|
|
155
|
-
{ ok: false, message: '内容违规' },
|
|
156
159
|
],
|
|
157
160
|
});
|
|
158
161
|
|
|
@@ -161,22 +164,28 @@ describe('weibo publish command', () => {
|
|
|
161
164
|
|
|
162
165
|
it('does not treat editor close as positive publish proof', async () => {
|
|
163
166
|
const command = getCommand();
|
|
167
|
+
// Step 8 polls up to SUBMIT_TIMEOUT_MS / SUBMIT_POLL_MS iterations
|
|
168
|
+
// (= 20000 / 500 = 40 in upstream). Derive the window programmatically
|
|
169
|
+
// so the test stays aligned with the implementation if the timeout
|
|
170
|
+
// changes, and override the makePage default { ok: true } fallback that
|
|
171
|
+
// would otherwise satisfy the success-marker break.
|
|
172
|
+
const SUBMIT_POLL_ITERATIONS = Math.ceil(20_000 / 500);
|
|
164
173
|
const page = makePage({
|
|
165
174
|
evaluateResults: [
|
|
166
175
|
'123456',
|
|
167
176
|
{ ok: true },
|
|
168
177
|
{ found: true, visible: true, rectTop: 100 },
|
|
169
178
|
{ ok: true, label: '发送' },
|
|
179
|
+
...Array(SUBMIT_POLL_ITERATIONS).fill(null),
|
|
170
180
|
],
|
|
171
181
|
evaluateWithArgsResults: [
|
|
172
182
|
{ ok: true, valueLength: 5 },
|
|
173
|
-
null,
|
|
174
183
|
],
|
|
175
184
|
});
|
|
176
185
|
|
|
177
186
|
await expect(command.func(page, { text: 'hello' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
178
187
|
|
|
179
|
-
const submitScript = page.
|
|
188
|
+
const submitScript = page.evaluate.mock.calls.at(-1)[0];
|
|
180
189
|
expect(submitScript).not.toContain('Editor closed after publish');
|
|
181
190
|
expect(submitScript).toContain('发布成功');
|
|
182
191
|
});
|
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) => ({
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weibo user-posts — list posts from a user within an optional date range.
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
6
|
+
import { unwrapEvaluateResult } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const MAX_LIMIT = 100;
|
|
9
|
+
const DEFAULT_LIMIT = 20;
|
|
10
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
11
|
+
|
|
12
|
+
function readRequiredId(raw) {
|
|
13
|
+
const value = String(raw ?? '').trim();
|
|
14
|
+
if (!value) {
|
|
15
|
+
throw new ArgumentError('weibo user-posts id cannot be empty');
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readLimit(raw) {
|
|
21
|
+
const value = raw === undefined || raw === null || raw === '' ? DEFAULT_LIMIT : Number(raw);
|
|
22
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_LIMIT) {
|
|
23
|
+
throw new ArgumentError(`weibo user-posts limit must be an integer between 1 and ${MAX_LIMIT}`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readDate(raw, name) {
|
|
29
|
+
if (raw === undefined || raw === null || raw === '') return null;
|
|
30
|
+
const value = String(raw).trim();
|
|
31
|
+
if (!DATE_RE.test(value)) {
|
|
32
|
+
throw new ArgumentError(`weibo user-posts ${name} must use YYYY-MM-DD`);
|
|
33
|
+
}
|
|
34
|
+
const date = new Date(`${value}T00:00:00+08:00`);
|
|
35
|
+
if (!Number.isFinite(date.getTime()) || value !== formatShanghaiDate(date)) {
|
|
36
|
+
throw new ArgumentError(`weibo user-posts ${name} must be a valid calendar date`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatShanghaiDate(date) {
|
|
42
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
43
|
+
timeZone: 'Asia/Shanghai',
|
|
44
|
+
year: 'numeric',
|
|
45
|
+
month: '2-digit',
|
|
46
|
+
day: '2-digit',
|
|
47
|
+
}).formatToParts(date);
|
|
48
|
+
const get = (type) => parts.find((part) => part.type === type)?.value;
|
|
49
|
+
return `${get('year')}-${get('month')}-${get('day')}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dateToTimestamp(date) {
|
|
53
|
+
return Math.floor(new Date(`${date}T00:00:00+08:00`).getTime() / 1000);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateRange(start, end) {
|
|
57
|
+
if (start && end && dateToTimestamp(start) > dateToTimestamp(end)) {
|
|
58
|
+
throw new ArgumentError('weibo user-posts start must be <= end');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mapError(error) {
|
|
63
|
+
const message = String(error ?? '').trim();
|
|
64
|
+
if (!message) {
|
|
65
|
+
throw new CommandExecutionError('weibo user-posts failed without an error message');
|
|
66
|
+
}
|
|
67
|
+
if (/login|cookie|登录|auth|forbidden|permission|权限|unauthorized/i.test(message)) {
|
|
68
|
+
throw new AuthRequiredError('weibo.com', message);
|
|
69
|
+
}
|
|
70
|
+
throw new CommandExecutionError(message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const testInternals = {
|
|
74
|
+
readRequiredId,
|
|
75
|
+
readLimit,
|
|
76
|
+
readDate,
|
|
77
|
+
dateToTimestamp,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
cli({
|
|
81
|
+
site: 'weibo',
|
|
82
|
+
name: 'user-posts',
|
|
83
|
+
access: 'read',
|
|
84
|
+
description: 'List Weibo posts from a user, optionally filtered by date range',
|
|
85
|
+
domain: 'weibo.com',
|
|
86
|
+
strategy: Strategy.COOKIE,
|
|
87
|
+
args: [
|
|
88
|
+
{ name: 'id', positional: true, required: true, help: 'User ID (numeric uid) or screen name' },
|
|
89
|
+
{ name: 'start', help: 'Start date in Asia/Shanghai (YYYY-MM-DD)' },
|
|
90
|
+
{ name: 'end', help: 'End date in Asia/Shanghai (YYYY-MM-DD)' },
|
|
91
|
+
{ name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of posts (1-${MAX_LIMIT})` },
|
|
92
|
+
{ name: 'include-retweets', type: 'boolean', default: false, help: 'Include retweets' },
|
|
93
|
+
],
|
|
94
|
+
columns: ['rank', 'id', 'mblogid', 'author', 'uid', 'text', 'time', 'reposts', 'comments', 'likes', 'pic_count', 'url'],
|
|
95
|
+
func: async (page, kwargs) => {
|
|
96
|
+
const id = readRequiredId(kwargs.id);
|
|
97
|
+
const limit = readLimit(kwargs.limit);
|
|
98
|
+
const start = readDate(kwargs.start, 'start');
|
|
99
|
+
const end = readDate(kwargs.end, 'end');
|
|
100
|
+
validateRange(start, end);
|
|
101
|
+
const includeRetweets = Boolean(kwargs['include-retweets']);
|
|
102
|
+
const starttime = start ? dateToTimestamp(start) : null;
|
|
103
|
+
const endtime = end ? dateToTimestamp(end) + 24 * 60 * 60 - 1 : null;
|
|
104
|
+
|
|
105
|
+
await page.goto('https://weibo.com');
|
|
106
|
+
await page.wait(2);
|
|
107
|
+
|
|
108
|
+
const evaluateResult = await page.evaluate(`
|
|
109
|
+
(async () => {
|
|
110
|
+
const rawId = ${JSON.stringify(id)};
|
|
111
|
+
const limit = ${limit};
|
|
112
|
+
const includeRetweets = ${includeRetweets};
|
|
113
|
+
const starttime = ${starttime === null ? 'null' : starttime};
|
|
114
|
+
const endtime = ${endtime === null ? 'null' : endtime};
|
|
115
|
+
const strip = (html) => (html || '')
|
|
116
|
+
.replace(/<[^>]+>/g, '')
|
|
117
|
+
.replace(/ /g, ' ')
|
|
118
|
+
.replace(/</g, '<')
|
|
119
|
+
.replace(/>/g, '>')
|
|
120
|
+
.replace(/&/g, '&')
|
|
121
|
+
.replace(/\\s+/g, ' ')
|
|
122
|
+
.trim();
|
|
123
|
+
|
|
124
|
+
async function readJson(url) {
|
|
125
|
+
const resp = await fetch(url, { credentials: 'include' });
|
|
126
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
127
|
+
return { error: 'login required: HTTP ' + resp.status };
|
|
128
|
+
}
|
|
129
|
+
if (!resp.ok) {
|
|
130
|
+
return { error: 'HTTP ' + resp.status };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return await resp.json();
|
|
134
|
+
} catch {
|
|
135
|
+
return { error: 'Malformed JSON response' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let uid = rawId;
|
|
140
|
+
if (!/^\\d+$/.test(rawId)) {
|
|
141
|
+
const profile = await readJson('/ajax/profile/info?screen_name=' + encodeURIComponent(rawId));
|
|
142
|
+
if (profile.error) return profile;
|
|
143
|
+
if (!profile.ok || !profile.data?.user?.id) return [rawId, [], true, false];
|
|
144
|
+
uid = String(profile.data.user.id);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const rows = [];
|
|
148
|
+
let sawList = false;
|
|
149
|
+
let sawPostCandidates = false;
|
|
150
|
+
for (let page = 1; page <= 20 && rows.length < limit; page++) {
|
|
151
|
+
const qs = new URLSearchParams();
|
|
152
|
+
qs.set('uid', uid);
|
|
153
|
+
qs.set('page', String(page));
|
|
154
|
+
qs.set('hasori', '1');
|
|
155
|
+
qs.set('hasret', includeRetweets ? '1' : '0');
|
|
156
|
+
if (starttime !== null) qs.set('starttime', String(starttime));
|
|
157
|
+
if (endtime !== null) qs.set('endtime', String(endtime));
|
|
158
|
+
|
|
159
|
+
const data = await readJson('/ajax/statuses/searchProfile?' + qs.toString());
|
|
160
|
+
if (data.error) return data;
|
|
161
|
+
if (data.ok === false) {
|
|
162
|
+
return { error: 'Weibo user posts API error: ' + (data.msg || data.message || 'request failed') };
|
|
163
|
+
}
|
|
164
|
+
const list = data.data?.list;
|
|
165
|
+
if (!Array.isArray(list)) {
|
|
166
|
+
return { error: 'Weibo user posts response did not include data.list' };
|
|
167
|
+
}
|
|
168
|
+
sawList = true;
|
|
169
|
+
if (list.length > 0) sawPostCandidates = true;
|
|
170
|
+
if (list.length === 0) break;
|
|
171
|
+
|
|
172
|
+
for (const post of list) {
|
|
173
|
+
if (rows.length >= limit) break;
|
|
174
|
+
const postId = post.idstr || (post.id === undefined || post.id === null ? '' : String(post.id));
|
|
175
|
+
const mblogid = post.mblogid || '';
|
|
176
|
+
const user = post.user || {};
|
|
177
|
+
const text = post.text_raw || strip(post.text || '');
|
|
178
|
+
if (!postId || !mblogid || !text) continue;
|
|
179
|
+
rows.push({
|
|
180
|
+
id: postId,
|
|
181
|
+
mblogid,
|
|
182
|
+
author: user.screen_name || '',
|
|
183
|
+
uid: user.id === undefined || user.id === null ? uid : String(user.id),
|
|
184
|
+
text,
|
|
185
|
+
time: post.created_at || '',
|
|
186
|
+
reposts: post.reposts_count ?? 0,
|
|
187
|
+
comments: post.comments_count ?? 0,
|
|
188
|
+
likes: post.attitudes_count ?? 0,
|
|
189
|
+
pic_count: post.pic_num ?? Object.keys(post.pic_infos || {}).length,
|
|
190
|
+
url: 'https://weibo.com/' + (user.id || uid) + '/' + mblogid,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (list.length < 10) break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return [uid, rows, sawList, sawPostCandidates];
|
|
198
|
+
})()
|
|
199
|
+
`);
|
|
200
|
+
|
|
201
|
+
const payload = unwrapEvaluateResult(evaluateResult);
|
|
202
|
+
if (payload && !Array.isArray(payload) && typeof payload === 'object' && 'error' in payload) {
|
|
203
|
+
mapError(payload.error);
|
|
204
|
+
}
|
|
205
|
+
if (!Array.isArray(payload) || payload.length !== 4 || !Array.isArray(payload[1])) {
|
|
206
|
+
throw new CommandExecutionError('weibo user-posts returned malformed extraction payload');
|
|
207
|
+
}
|
|
208
|
+
const [resolvedUid, rows, sawList, sawPostCandidates] = payload;
|
|
209
|
+
if (!sawList && rows.length === 0) {
|
|
210
|
+
throw new CommandExecutionError('weibo user-posts did not observe a valid posts list');
|
|
211
|
+
}
|
|
212
|
+
if (sawPostCandidates && rows.length === 0) {
|
|
213
|
+
throw new CommandExecutionError('weibo user-posts found post candidates but could not extract valid rows');
|
|
214
|
+
}
|
|
215
|
+
if (rows.length === 0) {
|
|
216
|
+
throw new EmptyResultError('weibo user-posts', 'No Weibo posts found for this user/date range');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return rows.slice(0, limit).map((row, index) => ({
|
|
220
|
+
rank: index + 1,
|
|
221
|
+
id: String(row.id),
|
|
222
|
+
mblogid: row.mblogid || '',
|
|
223
|
+
author: row.author || '',
|
|
224
|
+
uid: String(row.uid || resolvedUid || ''),
|
|
225
|
+
text: row.text || '',
|
|
226
|
+
time: row.time || '',
|
|
227
|
+
reposts: row.reposts ?? 0,
|
|
228
|
+
comments: row.comments ?? 0,
|
|
229
|
+
likes: row.likes ?? 0,
|
|
230
|
+
pic_count: row.pic_count ?? 0,
|
|
231
|
+
url: row.url || '',
|
|
232
|
+
}));
|
|
233
|
+
},
|
|
234
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import './user-posts.js';
|
|
5
|
+
import { testInternals } from './user-posts.js';
|
|
6
|
+
|
|
7
|
+
function envelope(data) {
|
|
8
|
+
return { session: 'site:weibo:test', data };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makePage(payload) {
|
|
12
|
+
return {
|
|
13
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
evaluate: vi.fn().mockResolvedValue(payload),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('weibo user-posts', () => {
|
|
20
|
+
const command = getRegistry().get('weibo/user-posts');
|
|
21
|
+
|
|
22
|
+
it('validates id, limit, dates, and date ranges before navigation', async () => {
|
|
23
|
+
await expect(command.func(makePage({ rows: [] }), { id: '', limit: 10 }))
|
|
24
|
+
.rejects.toBeInstanceOf(ArgumentError);
|
|
25
|
+
await expect(command.func(makePage({ rows: [] }), { id: '123', limit: 0 }))
|
|
26
|
+
.rejects.toBeInstanceOf(ArgumentError);
|
|
27
|
+
await expect(command.func(makePage({ rows: [] }), { id: '123', start: '2025-02-30', limit: 10 }))
|
|
28
|
+
.rejects.toBeInstanceOf(ArgumentError);
|
|
29
|
+
await expect(command.func(makePage({ rows: [] }), { id: '123', start: '2025-06-02', end: '2025-06-01', limit: 10 }))
|
|
30
|
+
.rejects.toBeInstanceOf(ArgumentError);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('converts Asia/Shanghai date boundaries to unix seconds', () => {
|
|
34
|
+
expect(testInternals.dateToTimestamp('2025-06-01')).toBe(1748707200);
|
|
35
|
+
expect(testInternals.dateToTimestamp('2025-01-01')).toBe(1735660800);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('unwraps browser bridge envelopes and returns stable listing rows', async () => {
|
|
39
|
+
const page = makePage(envelope(['1670458304', [{
|
|
40
|
+
id: '5012345678901234',
|
|
41
|
+
mblogid: 'QD5uq0ydj',
|
|
42
|
+
author: 'Alice',
|
|
43
|
+
uid: '1670458304',
|
|
44
|
+
text: 'hello',
|
|
45
|
+
time: 'Sun Jun 01 10:00:00 +0800 2025',
|
|
46
|
+
reposts: 1,
|
|
47
|
+
comments: 2,
|
|
48
|
+
likes: 3,
|
|
49
|
+
pic_count: 4,
|
|
50
|
+
url: 'https://weibo.com/1670458304/QD5uq0ydj',
|
|
51
|
+
}], true, true]));
|
|
52
|
+
|
|
53
|
+
await expect(command.func(page, {
|
|
54
|
+
id: '1670458304',
|
|
55
|
+
start: '2025-06-01',
|
|
56
|
+
end: '2025-06-02',
|
|
57
|
+
limit: 20,
|
|
58
|
+
})).resolves.toEqual([{
|
|
59
|
+
rank: 1,
|
|
60
|
+
id: '5012345678901234',
|
|
61
|
+
mblogid: 'QD5uq0ydj',
|
|
62
|
+
author: 'Alice',
|
|
63
|
+
uid: '1670458304',
|
|
64
|
+
text: 'hello',
|
|
65
|
+
time: 'Sun Jun 01 10:00:00 +0800 2025',
|
|
66
|
+
reposts: 1,
|
|
67
|
+
comments: 2,
|
|
68
|
+
likes: 3,
|
|
69
|
+
pic_count: 4,
|
|
70
|
+
url: 'https://weibo.com/1670458304/QD5uq0ydj',
|
|
71
|
+
}]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('maps auth-like evaluate errors to AuthRequiredError', async () => {
|
|
75
|
+
await expect(command.func(makePage({ error: 'login required: HTTP 403' }), { id: '123', limit: 10 }))
|
|
76
|
+
.rejects.toBeInstanceOf(AuthRequiredError);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('maps malformed payload and parser drift to CommandExecutionError', async () => {
|
|
80
|
+
await expect(command.func(makePage({ rows: [] }), { id: '123', limit: 10 }))
|
|
81
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
82
|
+
await expect(command.func(makePage({ error: 'Weibo user posts response did not include data.list' }), { id: '123', limit: 10 }))
|
|
83
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
84
|
+
await expect(command.func(makePage(['123', [], true, true]), { id: '123', limit: 10 }))
|
|
85
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('maps true empty lists to EmptyResultError', async () => {
|
|
89
|
+
await expect(command.func(makePage(['123', [], true, false]), { id: '123', limit: 10 }))
|
|
90
|
+
.rejects.toBeInstanceOf(EmptyResultError);
|
|
91
|
+
});
|
|
92
|
+
});
|
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;
|