@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import {
|
|
3
|
+
ArgumentError,
|
|
4
|
+
AuthRequiredError,
|
|
5
|
+
CommandExecutionError,
|
|
6
|
+
EmptyResultError,
|
|
7
|
+
} from '@jackwener/opencli/errors';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
const FLOMO_APP_DOMAIN = 'v.flomoapp.com';
|
|
11
|
+
const FLOMO_API_DOMAIN = 'flomoapp.com';
|
|
12
|
+
const MAX_LIMIT = 200;
|
|
13
|
+
|
|
14
|
+
function unwrapBrowserResult(value) {
|
|
15
|
+
if (value && typeof value === 'object' && 'session' in value && 'data' in value) {
|
|
16
|
+
return value.data;
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parsePositiveIntArg(value, name, fallback, max) {
|
|
22
|
+
if (value === undefined || value === null || value === '') {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
const text = String(value).trim();
|
|
26
|
+
if (!/^\d+$/.test(text)) {
|
|
27
|
+
throw new ArgumentError(`flomo memos --${name} must be a positive integer`);
|
|
28
|
+
}
|
|
29
|
+
const parsed = Number(text);
|
|
30
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > max) {
|
|
31
|
+
throw new ArgumentError(`flomo memos --${name} must be between 1 and ${max}`);
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseSinceArg(value) {
|
|
37
|
+
if (value === undefined || value === null || value === '') {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
const text = String(value).trim();
|
|
41
|
+
if (!/^\d+$/.test(text)) {
|
|
42
|
+
throw new ArgumentError('flomo memos --since must be a non-negative Unix timestamp in seconds');
|
|
43
|
+
}
|
|
44
|
+
const parsed = Number(text);
|
|
45
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
46
|
+
throw new ArgumentError('flomo memos --since must be a safe integer Unix timestamp in seconds');
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseSlugArg(value) {
|
|
52
|
+
if (value === undefined || value === null || value === '') {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
const slug = String(value).trim();
|
|
56
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/.test(slug)) {
|
|
57
|
+
throw new ArgumentError('flomo memos --slug must be an opaque memo cursor containing only letters, numbers, _ or -');
|
|
58
|
+
}
|
|
59
|
+
return slug;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildSignedUrl(limit, since, slug) {
|
|
63
|
+
const params = {
|
|
64
|
+
limit: String(limit),
|
|
65
|
+
latest_updated_at: String(since),
|
|
66
|
+
tz: '8:0',
|
|
67
|
+
timestamp: String(Math.floor(Date.now() / 1000)),
|
|
68
|
+
api_key: 'flomo_web',
|
|
69
|
+
app_version: '4.0',
|
|
70
|
+
platform: 'web',
|
|
71
|
+
webp: '1',
|
|
72
|
+
};
|
|
73
|
+
if (slug) params.latest_slug = slug;
|
|
74
|
+
const keys = Object.keys(params).sort();
|
|
75
|
+
const signBase = keys.map((key) => `${key}=${params[key]}`).join('&');
|
|
76
|
+
params.sign = createHash('md5').update(signBase + 'dbbc3dd73364b4084c3a69346e0ce2b2').digest('hex');
|
|
77
|
+
return 'https://flomoapp.com/api/v1/memo/updated/?' + new URLSearchParams(params).toString();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildGetTokenJs() {
|
|
81
|
+
return `
|
|
82
|
+
(() => {
|
|
83
|
+
try {
|
|
84
|
+
const raw = localStorage.getItem('me');
|
|
85
|
+
if (!raw) return null;
|
|
86
|
+
const me = JSON.parse(raw);
|
|
87
|
+
const token = me?.access_token || me?.data?.access_token || '';
|
|
88
|
+
return typeof token === 'string' && token.trim() ? token.trim() : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
})()
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isAuthFailureMessage(message) {
|
|
97
|
+
return /auth|unauth|login|token|permission|forbidden|unauthorized|登录|登陆|鉴权|权限/i.test(String(message || ''));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeTags(tags) {
|
|
101
|
+
if (!Array.isArray(tags)) return '';
|
|
102
|
+
return tags
|
|
103
|
+
.map((tag) => {
|
|
104
|
+
if (typeof tag === 'string') return tag;
|
|
105
|
+
return tag?.name || tag?.tag || tag?.content || '';
|
|
106
|
+
})
|
|
107
|
+
.map((tag) => String(tag).trim())
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(', ');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeImages(files) {
|
|
113
|
+
if (!Array.isArray(files)) return '';
|
|
114
|
+
return files
|
|
115
|
+
.map((file) => file?.thumbnail_url || file?.url || '')
|
|
116
|
+
.map((url) => String(url).trim())
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join(' | ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function memoUrl(slug) {
|
|
122
|
+
return slug ? `https://${FLOMO_APP_DOMAIN}/mine/?memo_id=${encodeURIComponent(slug)}` : '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeMemo(memo) {
|
|
126
|
+
if (!memo || typeof memo !== 'object' || Array.isArray(memo)) {
|
|
127
|
+
throw new CommandExecutionError('Flomo API returned a malformed memo entry');
|
|
128
|
+
}
|
|
129
|
+
const slug = String(memo.slug || memo.id || '').trim();
|
|
130
|
+
if (!slug) {
|
|
131
|
+
throw new CommandExecutionError('Flomo API returned a memo without slug/id');
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
id: slug,
|
|
135
|
+
url: memoUrl(slug),
|
|
136
|
+
content: String(memo.content || '').trim(),
|
|
137
|
+
slug,
|
|
138
|
+
tags: normalizeTags(memo.tags),
|
|
139
|
+
images: normalizeImages(memo.files),
|
|
140
|
+
created_at: String(memo.created_at || ''),
|
|
141
|
+
updated_at: String(memo.updated_at || ''),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function fetchFlomoJson(url, token) {
|
|
146
|
+
let resp;
|
|
147
|
+
try {
|
|
148
|
+
resp = await fetch(url, {
|
|
149
|
+
headers: {
|
|
150
|
+
Authorization: 'Bearer ' + token,
|
|
151
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
152
|
+
Accept: 'application/json',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
throw new CommandExecutionError(`Failed to fetch Flomo memos: ${err instanceof Error ? err.message : String(err)}`);
|
|
157
|
+
}
|
|
158
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
159
|
+
throw new AuthRequiredError(FLOMO_API_DOMAIN, `Flomo API returned HTTP ${resp.status}; please refresh your Flomo login session`);
|
|
160
|
+
}
|
|
161
|
+
if (!resp.ok) {
|
|
162
|
+
throw new CommandExecutionError(`Flomo API returned HTTP ${resp.status}`);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
return await resp.json();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
throw new CommandExecutionError(`Flomo API returned malformed JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function readAccessToken(page) {
|
|
172
|
+
const token = unwrapBrowserResult(await page.evaluate(buildGetTokenJs()));
|
|
173
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
174
|
+
throw new AuthRequiredError(FLOMO_API_DOMAIN, 'Flomo memos requires an active signed-in Flomo browser session');
|
|
175
|
+
}
|
|
176
|
+
return token.trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const command = cli({
|
|
180
|
+
site: 'flomo',
|
|
181
|
+
name: 'memos',
|
|
182
|
+
access: 'read',
|
|
183
|
+
description: 'List your Flomo memos',
|
|
184
|
+
domain: FLOMO_API_DOMAIN,
|
|
185
|
+
strategy: Strategy.COOKIE,
|
|
186
|
+
browser: true,
|
|
187
|
+
navigateBefore: `https://${FLOMO_APP_DOMAIN}/`,
|
|
188
|
+
args: [
|
|
189
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of memos to fetch (1-200)' },
|
|
190
|
+
{ name: 'since', type: 'int', help: 'Only memos updated after this Unix timestamp in seconds' },
|
|
191
|
+
{ name: 'slug', help: 'Pagination cursor from a previous memo page' },
|
|
192
|
+
],
|
|
193
|
+
columns: ['id', 'url', 'content', 'slug', 'tags', 'images', 'created_at', 'updated_at'],
|
|
194
|
+
func: async (page, kwargs) => {
|
|
195
|
+
const limit = parsePositiveIntArg(kwargs.limit, 'limit', 20, MAX_LIMIT);
|
|
196
|
+
const since = parseSinceArg(kwargs.since);
|
|
197
|
+
const slug = parseSlugArg(kwargs.slug);
|
|
198
|
+
await page.wait(3).catch(() => {});
|
|
199
|
+
const token = await readAccessToken(page);
|
|
200
|
+
const body = await fetchFlomoJson(buildSignedUrl(limit, since, slug), token);
|
|
201
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
202
|
+
throw new CommandExecutionError('Flomo API returned a malformed response');
|
|
203
|
+
}
|
|
204
|
+
if (body.code !== 0) {
|
|
205
|
+
const message = body.message || `Flomo API error code ${body.code}`;
|
|
206
|
+
if (isAuthFailureMessage(message)) {
|
|
207
|
+
throw new AuthRequiredError(FLOMO_API_DOMAIN, message);
|
|
208
|
+
}
|
|
209
|
+
throw new CommandExecutionError(message);
|
|
210
|
+
}
|
|
211
|
+
if (!Array.isArray(body.data)) {
|
|
212
|
+
throw new CommandExecutionError('Flomo API returned malformed memo data');
|
|
213
|
+
}
|
|
214
|
+
if (body.data.length === 0) {
|
|
215
|
+
throw new EmptyResultError('flomo memos', 'No Flomo memos matched the requested filters.');
|
|
216
|
+
}
|
|
217
|
+
return body.data.map(normalizeMemo);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export const __test__ = {
|
|
222
|
+
buildSignedUrl,
|
|
223
|
+
command,
|
|
224
|
+
normalizeMemo,
|
|
225
|
+
parsePositiveIntArg,
|
|
226
|
+
parseSinceArg,
|
|
227
|
+
parseSlugArg,
|
|
228
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
ArgumentError,
|
|
4
|
+
AuthRequiredError,
|
|
5
|
+
CommandExecutionError,
|
|
6
|
+
EmptyResultError,
|
|
7
|
+
} from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
const { __test__ } = await import('./memos.js');
|
|
10
|
+
const { command, normalizeMemo, parsePositiveIntArg, parseSinceArg, parseSlugArg } = __test__;
|
|
11
|
+
|
|
12
|
+
function createPage(token = 'token-123') {
|
|
13
|
+
return {
|
|
14
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
evaluate: vi.fn().mockResolvedValue({ session: 'browser:default', data: token }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mockFetchJson(body, status = 200) {
|
|
20
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
21
|
+
ok: status >= 200 && status < 300,
|
|
22
|
+
status,
|
|
23
|
+
json: vi.fn().mockResolvedValue(body),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('flomo memos registration', () => {
|
|
28
|
+
it('registers as a browser cookie read command with stable columns', () => {
|
|
29
|
+
expect(command.site).toBe('flomo');
|
|
30
|
+
expect(command.name).toBe('memos');
|
|
31
|
+
expect(command.access).toBe('read');
|
|
32
|
+
expect(command.browser).toBe(true);
|
|
33
|
+
expect(command.strategy).toBe('cookie');
|
|
34
|
+
expect(command.columns).toEqual(['id', 'url', 'content', 'slug', 'tags', 'images', 'created_at', 'updated_at']);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('flomo memos argument validation', () => {
|
|
39
|
+
it('rejects invalid limits instead of silently clamping', () => {
|
|
40
|
+
expect(() => parsePositiveIntArg('0', 'limit', 20, 200)).toThrow(ArgumentError);
|
|
41
|
+
expect(() => parsePositiveIntArg('201', 'limit', 20, 200)).toThrow(ArgumentError);
|
|
42
|
+
expect(() => parsePositiveIntArg('10.5', 'limit', 20, 200)).toThrow(ArgumentError);
|
|
43
|
+
expect(() => parsePositiveIntArg('abc', 'limit', 20, 200)).toThrow(ArgumentError);
|
|
44
|
+
expect(parsePositiveIntArg(undefined, 'limit', 20, 200)).toBe(20);
|
|
45
|
+
expect(parsePositiveIntArg('200', 'limit', 20, 200)).toBe(200);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rejects invalid since and slug arguments', () => {
|
|
49
|
+
expect(parseSinceArg(undefined)).toBe(0);
|
|
50
|
+
expect(parseSinceArg('1735689600')).toBe(1735689600);
|
|
51
|
+
expect(() => parseSinceArg('-1')).toThrow(ArgumentError);
|
|
52
|
+
expect(() => parseSinceArg('1.5')).toThrow(ArgumentError);
|
|
53
|
+
expect(parseSlugArg(undefined)).toBe('');
|
|
54
|
+
expect(parseSlugArg('abc_DEF-123')).toBe('abc_DEF-123');
|
|
55
|
+
expect(() => parseSlugArg('bad/slash')).toThrow(ArgumentError);
|
|
56
|
+
expect(() => parseSlugArg('bad space')).toThrow(ArgumentError);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('flomo memo normalization', () => {
|
|
61
|
+
it('emits string-safe id/url fields and normalizes tags/images', () => {
|
|
62
|
+
expect(normalizeMemo({
|
|
63
|
+
slug: 'memo_12345678901234567890',
|
|
64
|
+
content: ' <p>Hello</p> ',
|
|
65
|
+
tags: [{ name: 'work' }, 'idea'],
|
|
66
|
+
files: [{ thumbnail_url: 'https://img/thumb.jpg' }, { url: 'https://img/full.jpg' }],
|
|
67
|
+
created_at: '2026-01-01T00:00:00+08:00',
|
|
68
|
+
updated_at: '2026-01-02T00:00:00+08:00',
|
|
69
|
+
})).toEqual({
|
|
70
|
+
id: 'memo_12345678901234567890',
|
|
71
|
+
url: 'https://v.flomoapp.com/mine/?memo_id=memo_12345678901234567890',
|
|
72
|
+
content: '<p>Hello</p>',
|
|
73
|
+
slug: 'memo_12345678901234567890',
|
|
74
|
+
tags: 'work, idea',
|
|
75
|
+
images: 'https://img/thumb.jpg | https://img/full.jpg',
|
|
76
|
+
created_at: '2026-01-01T00:00:00+08:00',
|
|
77
|
+
updated_at: '2026-01-02T00:00:00+08:00',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('fails typed on malformed memo entries', () => {
|
|
82
|
+
expect(() => normalizeMemo(null)).toThrow(CommandExecutionError);
|
|
83
|
+
expect(() => normalizeMemo({ content: 'missing slug' })).toThrow(CommandExecutionError);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('flomo memos command', () => {
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.unstubAllGlobals();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('reads token from Browser Bridge envelope and returns memo rows', async () => {
|
|
93
|
+
mockFetchJson({
|
|
94
|
+
code: 0,
|
|
95
|
+
data: [{
|
|
96
|
+
slug: 'memo_1',
|
|
97
|
+
content: 'hello',
|
|
98
|
+
tags: ['tag'],
|
|
99
|
+
files: [],
|
|
100
|
+
created_at: '2026-01-01',
|
|
101
|
+
updated_at: '2026-01-02',
|
|
102
|
+
}],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const rows = await command.func(createPage(), { limit: '1' });
|
|
106
|
+
|
|
107
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(expect.stringContaining('limit=1'), expect.objectContaining({
|
|
108
|
+
headers: expect.objectContaining({ Authorization: 'Bearer token-123' }),
|
|
109
|
+
}));
|
|
110
|
+
expect(rows).toEqual([{
|
|
111
|
+
id: 'memo_1',
|
|
112
|
+
url: 'https://v.flomoapp.com/mine/?memo_id=memo_1',
|
|
113
|
+
content: 'hello',
|
|
114
|
+
slug: 'memo_1',
|
|
115
|
+
tags: 'tag',
|
|
116
|
+
images: '',
|
|
117
|
+
created_at: '2026-01-01',
|
|
118
|
+
updated_at: '2026-01-02',
|
|
119
|
+
}]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('throws AuthRequiredError when the browser session has no token', async () => {
|
|
123
|
+
await expect(command.func(createPage(null), {})).rejects.toBeInstanceOf(AuthRequiredError);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('maps Flomo auth failures to AuthRequiredError', async () => {
|
|
127
|
+
mockFetchJson({ code: 401, message: 'unauthorized' });
|
|
128
|
+
await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(AuthRequiredError);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('maps HTTP, malformed JSON, malformed data, and empty results to typed errors', async () => {
|
|
132
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500, json: vi.fn() }));
|
|
133
|
+
await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(CommandExecutionError);
|
|
134
|
+
|
|
135
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200, json: vi.fn().mockRejectedValue(new Error('bad json')) }));
|
|
136
|
+
await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(CommandExecutionError);
|
|
137
|
+
|
|
138
|
+
mockFetchJson({ code: 0, data: {} });
|
|
139
|
+
await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(CommandExecutionError);
|
|
140
|
+
|
|
141
|
+
mockFetchJson({ code: 0, data: [] });
|
|
142
|
+
await expect(command.func(createPage(), {})).rejects.toBeInstanceOf(EmptyResultError);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/clis/gitee/search.js
CHANGED
|
@@ -123,8 +123,8 @@ cli({
|
|
|
123
123
|
rows.push({
|
|
124
124
|
rank: rows.length + 1,
|
|
125
125
|
name,
|
|
126
|
-
language: normalizeText(getFirstText(fields.langs)) || '
|
|
127
|
-
description: normalizeText(getFirstText(fields.description)) || '
|
|
126
|
+
language: normalizeText(getFirstText(fields.langs)) || '',
|
|
127
|
+
description: normalizeText(getFirstText(fields.description)) || '',
|
|
128
128
|
stars: normalizeStars(fields['count.star']),
|
|
129
129
|
url: repoUrl,
|
|
130
130
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import './search.js';
|
|
4
|
+
|
|
5
|
+
function mockGiteeResponse(hits) {
|
|
6
|
+
return {
|
|
7
|
+
ok: true,
|
|
8
|
+
json: () => Promise.resolve({ hits: { hits } }),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function makePage() {
|
|
13
|
+
return {
|
|
14
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('gitee search', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.unstubAllGlobals();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('emits empty-string for missing language / description instead of a sentinel', async () => {
|
|
28
|
+
const cmd = getRegistry().get('gitee/search');
|
|
29
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
30
|
+
const fetchMock = vi.fn().mockResolvedValue(mockGiteeResponse([
|
|
31
|
+
{
|
|
32
|
+
fields: {
|
|
33
|
+
title: 'someuser/no-meta-repo',
|
|
34
|
+
url: 'https://gitee.com/someuser/no-meta-repo',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
]));
|
|
38
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
39
|
+
const rows = await cmd.func(makePage(), { keyword: 'test', limit: 10 });
|
|
40
|
+
expect(rows).toHaveLength(1);
|
|
41
|
+
expect(rows[0].name).toBe('someuser/no-meta-repo');
|
|
42
|
+
expect(rows[0].language).toBe('');
|
|
43
|
+
expect(rows[0].description).toBe('');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('passes through populated language / description verbatim', async () => {
|
|
47
|
+
const cmd = getRegistry().get('gitee/search');
|
|
48
|
+
const fetchMock = vi.fn().mockResolvedValue(mockGiteeResponse([
|
|
49
|
+
{
|
|
50
|
+
fields: {
|
|
51
|
+
title: 'org/repo-a',
|
|
52
|
+
url: 'https://gitee.com/org/repo-a',
|
|
53
|
+
langs: 'TypeScript',
|
|
54
|
+
description: 'A test repo',
|
|
55
|
+
'count.star': '42',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
]));
|
|
59
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
60
|
+
const rows = await cmd.func(makePage(), { keyword: 'test', limit: 10 });
|
|
61
|
+
expect(rows[0].language).toBe('TypeScript');
|
|
62
|
+
expect(rows[0].description).toBe('A test repo');
|
|
63
|
+
expect(rows[0].stars).toBe('42');
|
|
64
|
+
});
|
|
65
|
+
});
|
package/clis/jike/post.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
3
|
cli({
|
|
3
4
|
site: 'jike',
|
|
4
5
|
name: 'post',
|
|
@@ -16,16 +17,16 @@ cli({
|
|
|
16
17
|
},
|
|
17
18
|
],
|
|
18
19
|
columns: ['type', 'author', 'content', 'likes', 'time'],
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
func: async (page, args) => {
|
|
21
|
+
await page.goto(`https://m.okjike.com/originalPosts/${args.id}`);
|
|
22
|
+
const data = await page.evaluate(`(() => {
|
|
23
|
+
const el = document.querySelector('script[type="application/json"]');
|
|
24
|
+
if (!el) return { ok: false, reason: 'missing-data-script' };
|
|
22
25
|
try {
|
|
23
|
-
const
|
|
24
|
-
if (!el) return [];
|
|
25
|
-
const data = JSON.parse(el.textContent);
|
|
26
|
+
const data = JSON.parse(el.textContent || '{}');
|
|
26
27
|
const pageProps = data?.props?.pageProps || {};
|
|
27
28
|
const post = pageProps.post || {};
|
|
28
|
-
const comments = pageProps.comments
|
|
29
|
+
const comments = Array.isArray(pageProps.comments) ? pageProps.comments : [];
|
|
29
30
|
|
|
30
31
|
const result = [{
|
|
31
32
|
type: 'post',
|
|
@@ -47,16 +48,25 @@ cli({
|
|
|
47
48
|
|
|
48
49
|
return result;
|
|
49
50
|
} catch (e) {
|
|
50
|
-
return
|
|
51
|
+
return { ok: false, reason: 'parse-error', message: e?.message || String(e) };
|
|
51
52
|
}
|
|
52
53
|
})()
|
|
53
|
-
`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
`);
|
|
55
|
+
if (Array.isArray(data)) {
|
|
56
|
+
return data.map((item) => ({
|
|
57
|
+
type: item.type ?? '',
|
|
58
|
+
author: item.author ?? '',
|
|
59
|
+
content: item.content ?? '',
|
|
60
|
+
likes: item.likes ?? 0,
|
|
61
|
+
time: item.time ?? '',
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
if (data?.reason === 'missing-data-script') {
|
|
65
|
+
throw new CommandExecutionError('Jike post page did not expose the expected data script');
|
|
66
|
+
}
|
|
67
|
+
if (data?.reason === 'parse-error') {
|
|
68
|
+
throw new CommandExecutionError(`Failed to parse Jike post data: ${data.message || 'unknown error'}`);
|
|
69
|
+
}
|
|
70
|
+
throw new CommandExecutionError('Jike post returned an unreadable payload');
|
|
71
|
+
},
|
|
62
72
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
import './post.js';
|
|
5
|
+
import './topic.js';
|
|
6
|
+
import './user.js';
|
|
7
|
+
|
|
8
|
+
function makePage(evaluateResult) {
|
|
9
|
+
return {
|
|
10
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('jike read commands', () => {
|
|
16
|
+
it('maps post rows from the browser-side extractor', async () => {
|
|
17
|
+
const command = getRegistry().get('jike/post');
|
|
18
|
+
const page = makePage([
|
|
19
|
+
{ type: 'post', author: 'alice', content: 'hello', likes: 3, time: '2026-05-16' },
|
|
20
|
+
{ type: 'comment', author: 'bob', content: 'nice', likes: 1, time: '2026-05-16' },
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
await expect(command.func(page, { id: 'post-1' })).resolves.toEqual([
|
|
24
|
+
{ type: 'post', author: 'alice', content: 'hello', likes: 3, time: '2026-05-16' },
|
|
25
|
+
{ type: 'comment', author: 'bob', content: 'nice', likes: 1, time: '2026-05-16' },
|
|
26
|
+
]);
|
|
27
|
+
expect(page.goto).toHaveBeenCalledWith('https://m.okjike.com/originalPosts/post-1');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('maps topic rows and applies limit on the Node side', async () => {
|
|
31
|
+
const command = getRegistry().get('jike/topic');
|
|
32
|
+
const page = makePage([
|
|
33
|
+
{ id: 'a', content: 'one', author: 'alice', likes: 1, comments: 2, time: 't1' },
|
|
34
|
+
{ id: 'b', content: 'two', author: 'bob', likes: 3, comments: 4, time: 't2' },
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
await expect(command.func(page, { id: 'topic-1', limit: 1 })).resolves.toEqual([
|
|
38
|
+
{
|
|
39
|
+
content: 'one',
|
|
40
|
+
author: 'alice',
|
|
41
|
+
likes: 1,
|
|
42
|
+
comments: 2,
|
|
43
|
+
time: 't1',
|
|
44
|
+
url: 'https://web.okjike.com/originalPost/a',
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
expect(page.goto).toHaveBeenCalledWith('https://m.okjike.com/topics/topic-1');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('maps user rows and applies limit on the Node side', async () => {
|
|
51
|
+
const command = getRegistry().get('jike/user');
|
|
52
|
+
const page = makePage([
|
|
53
|
+
{ id: 'a', content: 'one', type: 'post', likes: 1, comments: 2, time: 't1' },
|
|
54
|
+
{ id: 'b', content: 'two', type: 'repost', likes: 3, comments: 4, time: 't2' },
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
await expect(command.func(page, { username: 'alice', limit: 1 })).resolves.toEqual([
|
|
58
|
+
{
|
|
59
|
+
id: 'a',
|
|
60
|
+
content: 'one',
|
|
61
|
+
type: 'post',
|
|
62
|
+
likes: 1,
|
|
63
|
+
comments: 2,
|
|
64
|
+
time: 't1',
|
|
65
|
+
url: 'https://web.okjike.com/originalPost/a',
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
expect(page.goto).toHaveBeenCalledWith('https://m.okjike.com/users/alice');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('throws CommandExecutionError for malformed browser-side payloads', async () => {
|
|
72
|
+
await expect(getRegistry().get('jike/post').func(makePage({ reason: 'missing-data-script' }), { id: 'post-1' }))
|
|
73
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
74
|
+
await expect(getRegistry().get('jike/topic').func(makePage({ reason: 'parse-error', message: 'bad json' }), { id: 'topic-1' }))
|
|
75
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
76
|
+
await expect(getRegistry().get('jike/user').func(makePage(null), { username: 'alice' }))
|
|
77
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('throws EmptyResultError when topic or user extractors return no posts', async () => {
|
|
81
|
+
await expect(getRegistry().get('jike/topic').func(makePage([]), { id: 'topic-1' }))
|
|
82
|
+
.rejects.toBeInstanceOf(EmptyResultError);
|
|
83
|
+
await expect(getRegistry().get('jike/user').func(makePage([]), { username: 'alice' }))
|
|
84
|
+
.rejects.toBeInstanceOf(EmptyResultError);
|
|
85
|
+
});
|
|
86
|
+
});
|
package/clis/jike/topic.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
3
|
cli({
|
|
3
4
|
site: 'jike',
|
|
4
5
|
name: 'topic',
|
|
@@ -17,15 +18,16 @@ cli({
|
|
|
17
18
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of posts' },
|
|
18
19
|
],
|
|
19
20
|
columns: ['content', 'author', 'likes', 'comments', 'time', 'url'],
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
func: async (page, args) => {
|
|
22
|
+
await page.goto(`https://m.okjike.com/topics/${args.id}`);
|
|
23
|
+
const limit = Number(args.limit) || 20;
|
|
24
|
+
const data = await page.evaluate(`(() => {
|
|
25
|
+
const el = document.querySelector('script[type="application/json"]');
|
|
26
|
+
if (!el) return { ok: false, reason: 'missing-data-script' };
|
|
23
27
|
try {
|
|
24
|
-
const
|
|
25
|
-
if (!el) return [];
|
|
26
|
-
const data = JSON.parse(el.textContent);
|
|
28
|
+
const data = JSON.parse(el.textContent || '{}');
|
|
27
29
|
const pageProps = data?.props?.pageProps || {};
|
|
28
|
-
const posts = pageProps.posts
|
|
30
|
+
const posts = Array.isArray(pageProps.posts) ? pageProps.posts : [];
|
|
29
31
|
return posts.map(p => ({
|
|
30
32
|
content: (p.content || '').replace(/\\n/g, ' ').slice(0, 80),
|
|
31
33
|
author: p.user?.screenName || '',
|
|
@@ -35,18 +37,29 @@ cli({
|
|
|
35
37
|
id: p.id || '',
|
|
36
38
|
}));
|
|
37
39
|
} catch (e) {
|
|
38
|
-
return
|
|
40
|
+
return { ok: false, reason: 'parse-error', message: e?.message || String(e) };
|
|
39
41
|
}
|
|
40
42
|
})()
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
`);
|
|
44
|
+
if (Array.isArray(data)) {
|
|
45
|
+
if (data.length === 0) {
|
|
46
|
+
throw new EmptyResultError('jike topic', `No posts were returned for topic ${args.id}. Confirm the topic ID and login state.`);
|
|
47
|
+
}
|
|
48
|
+
return data.slice(0, limit).map((item) => ({
|
|
49
|
+
content: item.content ?? '',
|
|
50
|
+
author: item.author ?? '',
|
|
51
|
+
likes: item.likes ?? 0,
|
|
52
|
+
comments: item.comments ?? 0,
|
|
53
|
+
time: item.time ?? '',
|
|
54
|
+
url: `https://web.okjike.com/originalPost/${item.id ?? ''}`,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
if (data?.reason === 'missing-data-script') {
|
|
58
|
+
throw new CommandExecutionError('Jike topic page did not expose the expected data script');
|
|
59
|
+
}
|
|
60
|
+
if (data?.reason === 'parse-error') {
|
|
61
|
+
throw new CommandExecutionError(`Failed to parse Jike topic data: ${data.message || 'unknown error'}`);
|
|
62
|
+
}
|
|
63
|
+
throw new CommandExecutionError('Jike topic returned an unreadable payload');
|
|
64
|
+
},
|
|
52
65
|
});
|