@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/utils.js
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared Weibo utilities — uid extraction.
|
|
3
3
|
*/
|
|
4
|
-
import { AuthRequiredError } from '@jackwener/opencli/errors';
|
|
4
|
+
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
5
|
+
/**
|
|
6
|
+
* `page.evaluate` may return either the raw IIFE value or a
|
|
7
|
+
* `{ session, data }` envelope depending on the browser-bridge version.
|
|
8
|
+
* Adapter code that inspected the payload directly (e.g. `Array.isArray`,
|
|
9
|
+
* truthiness checks on uid strings) silently received the envelope wrapper
|
|
10
|
+
* instead of the inner value. This helper normalizes both shapes so callers
|
|
11
|
+
* can keep their existing checks unchanged.
|
|
12
|
+
*/
|
|
13
|
+
export function unwrapEvaluateResult(payload) {
|
|
14
|
+
if (payload && !Array.isArray(payload) && typeof payload === 'object' && 'session' in payload && 'data' in payload) {
|
|
15
|
+
return payload.data;
|
|
16
|
+
}
|
|
17
|
+
return payload;
|
|
18
|
+
}
|
|
19
|
+
export function requireArrayEvaluateResult(payload, label) {
|
|
20
|
+
if (!Array.isArray(payload)) {
|
|
21
|
+
if (payload && typeof payload === 'object' && 'error' in payload) {
|
|
22
|
+
throw new CommandExecutionError(`${label}: ${String(payload.error)}`);
|
|
23
|
+
}
|
|
24
|
+
throw new CommandExecutionError(`${label} returned malformed extraction payload`);
|
|
25
|
+
}
|
|
26
|
+
return payload;
|
|
27
|
+
}
|
|
28
|
+
export function requireObjectEvaluateResult(payload, label) {
|
|
29
|
+
if (!payload || Array.isArray(payload) || typeof payload !== 'object') {
|
|
30
|
+
throw new CommandExecutionError(`${label} returned malformed extraction payload`);
|
|
31
|
+
}
|
|
32
|
+
return payload;
|
|
33
|
+
}
|
|
5
34
|
/** Get the currently logged-in user's uid from Vue store or config API. */
|
|
6
35
|
export async function getSelfUid(page) {
|
|
7
|
-
const uid = await page.evaluate(`
|
|
36
|
+
const uid = unwrapEvaluateResult(await page.evaluate(`
|
|
8
37
|
(() => {
|
|
9
38
|
const app = document.querySelector('#app')?.__vue_app__;
|
|
10
39
|
const store = app?.config?.globalProperties?.$store;
|
|
@@ -12,18 +41,18 @@ export async function getSelfUid(page) {
|
|
|
12
41
|
if (uid) return String(uid);
|
|
13
42
|
return null;
|
|
14
43
|
})()
|
|
15
|
-
`);
|
|
44
|
+
`));
|
|
16
45
|
if (uid)
|
|
17
46
|
return uid;
|
|
18
47
|
// Fallback: config API
|
|
19
|
-
const config = await page.evaluate(`
|
|
48
|
+
const config = unwrapEvaluateResult(await page.evaluate(`
|
|
20
49
|
(async () => {
|
|
21
50
|
const resp = await fetch('/ajax/config/get_config', {credentials: 'include'});
|
|
22
51
|
if (!resp.ok) return null;
|
|
23
52
|
const data = await resp.json();
|
|
24
53
|
return data.ok && data.data?.uid ? String(data.data.uid) : null;
|
|
25
54
|
})()
|
|
26
|
-
`);
|
|
55
|
+
`));
|
|
27
56
|
if (config)
|
|
28
57
|
return config;
|
|
29
58
|
throw new AuthRequiredError('weibo.com');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { requireArrayEvaluateResult, requireObjectEvaluateResult, unwrapEvaluateResult } from './utils.js';
|
|
4
|
+
|
|
5
|
+
describe('unwrapEvaluateResult (browser-bridge envelope normalization)', () => {
|
|
6
|
+
it('returns the raw array unchanged when payload is already an array', () => {
|
|
7
|
+
const arr = [{ id: '1' }, { id: '2' }];
|
|
8
|
+
expect(unwrapEvaluateResult(arr)).toBe(arr);
|
|
9
|
+
});
|
|
10
|
+
it('unwraps { session, data: [...] } envelope to the inner array', () => {
|
|
11
|
+
const arr = [{ id: '1' }];
|
|
12
|
+
const env = { session: 'site:weibo:abc', data: arr };
|
|
13
|
+
expect(unwrapEvaluateResult(env)).toBe(arr);
|
|
14
|
+
});
|
|
15
|
+
it('unwraps primitive data (e.g. uid string) from Browser Bridge envelopes', () => {
|
|
16
|
+
expect(unwrapEvaluateResult({ session: 'site:weibo:abc', data: '1234567890' })).toBe('1234567890');
|
|
17
|
+
});
|
|
18
|
+
it('unwraps null payload data so getSelfUid fallback can trigger', () => {
|
|
19
|
+
expect(unwrapEvaluateResult({ session: 'site:weibo:abc', data: null })).toBe(null);
|
|
20
|
+
});
|
|
21
|
+
it('passes non-envelope objects through unchanged (e.g. profile result)', () => {
|
|
22
|
+
const obj = { screen_name: 'alice', uid: '42' };
|
|
23
|
+
expect(unwrapEvaluateResult(obj)).toBe(obj);
|
|
24
|
+
});
|
|
25
|
+
it('handles null and undefined safely', () => {
|
|
26
|
+
expect(unwrapEvaluateResult(null)).toBe(null);
|
|
27
|
+
expect(unwrapEvaluateResult(undefined)).toBe(undefined);
|
|
28
|
+
});
|
|
29
|
+
it('keeps malformed array/object payloads as typed command failures after unwrap', () => {
|
|
30
|
+
expect(requireArrayEvaluateResult([{ id: '1' }], 'weibo feed')).toEqual([{ id: '1' }]);
|
|
31
|
+
expect(() => requireArrayEvaluateResult({ error: 'API error' }, 'weibo feed')).toThrow(CommandExecutionError);
|
|
32
|
+
expect(() => requireArrayEvaluateResult({ error: 'API error' }, 'weibo feed')).toThrow('weibo feed: API error');
|
|
33
|
+
expect(requireObjectEvaluateResult({ uid: '42' }, 'weibo me')).toEqual({ uid: '42' });
|
|
34
|
+
expect(() => requireObjectEvaluateResult([{ uid: '42' }], 'weibo me')).toThrow(CommandExecutionError);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
4
|
import './search.js';
|
|
4
5
|
describe('weread/search regression', () => {
|
|
5
6
|
beforeEach(() => {
|
|
@@ -146,7 +147,7 @@ describe('weread/search regression', () => {
|
|
|
146
147
|
},
|
|
147
148
|
]);
|
|
148
149
|
});
|
|
149
|
-
it('
|
|
150
|
+
it('surfaces search html request failures instead of emitting empty urls', async () => {
|
|
150
151
|
const command = getRegistry().get('weread/search');
|
|
151
152
|
expect(command?.func).toBeTypeOf('function');
|
|
152
153
|
const fetchMock = vi.fn()
|
|
@@ -166,16 +167,22 @@ describe('weread/search regression', () => {
|
|
|
166
167
|
})
|
|
167
168
|
.mockRejectedValueOnce(new Error('network timeout'));
|
|
168
169
|
vi.stubGlobal('fetch', fetchMock);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
|
|
170
|
+
await expect(command.func({ query: 'deep work', limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
171
|
+
});
|
|
172
|
+
it('throws EmptyResultError when the public search API returns no books', async () => {
|
|
173
|
+
const command = getRegistry().get('weread/search');
|
|
174
|
+
expect(command?.func).toBeTypeOf('function');
|
|
175
|
+
const fetchMock = vi.fn()
|
|
176
|
+
.mockResolvedValueOnce({
|
|
177
|
+
ok: true,
|
|
178
|
+
json: () => Promise.resolve({ books: [] }),
|
|
179
|
+
})
|
|
180
|
+
.mockResolvedValueOnce({
|
|
181
|
+
ok: true,
|
|
182
|
+
text: () => Promise.resolve('<html></html>'),
|
|
183
|
+
});
|
|
184
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
185
|
+
await expect(command.func({ query: 'definitely-missing-book', limit: 5 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
179
186
|
});
|
|
180
187
|
it('binds reader urls with title and author instead of title alone', async () => {
|
|
181
188
|
const command = getRegistry().get('weread/search');
|
package/clis/weread/search.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { fetchWebApi, WEREAD_UA, WEREAD_WEB_ORIGIN } from './utils.js';
|
|
3
4
|
function decodeHtmlText(value) {
|
|
4
5
|
return value
|
|
@@ -84,18 +85,19 @@ function resolveSearchResultUrl(params) {
|
|
|
84
85
|
async function loadSearchHtmlEntries(query) {
|
|
85
86
|
const url = new URL('/web/search/books', WEREAD_WEB_ORIGIN);
|
|
86
87
|
url.searchParams.set('keyword', query);
|
|
87
|
-
let
|
|
88
|
+
let resp;
|
|
88
89
|
try {
|
|
89
|
-
|
|
90
|
+
resp = await fetch(url.toString(), {
|
|
90
91
|
headers: { 'User-Agent': WEREAD_UA },
|
|
91
92
|
});
|
|
92
|
-
if (!resp.ok)
|
|
93
|
-
return [];
|
|
94
|
-
html = await resp.text();
|
|
95
93
|
}
|
|
96
|
-
catch {
|
|
97
|
-
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new CommandExecutionError(`Failed to fetch WeRead search page: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
96
|
}
|
|
97
|
+
if (!resp.ok) {
|
|
98
|
+
throw new CommandExecutionError(`WeRead search page request failed: HTTP ${resp.status}`);
|
|
99
|
+
}
|
|
100
|
+
const html = await resp.text();
|
|
99
101
|
const items = Array.from(html.matchAll(/<li[^>]*class="wr_bookList_item"[^>]*>([\s\S]*?)<\/li>/g));
|
|
100
102
|
return items.map((match) => {
|
|
101
103
|
const chunk = match[1];
|
|
@@ -131,6 +133,12 @@ cli({
|
|
|
131
133
|
loadSearchHtmlEntries(String(args.query ?? '')),
|
|
132
134
|
]);
|
|
133
135
|
const books = data?.books ?? [];
|
|
136
|
+
if (!Array.isArray(books)) {
|
|
137
|
+
throw new CommandExecutionError('WeRead search API returned an unreadable books payload');
|
|
138
|
+
}
|
|
139
|
+
if (books.length === 0) {
|
|
140
|
+
throw new EmptyResultError('weread search', `No books were returned for query ${args.query}.`);
|
|
141
|
+
}
|
|
134
142
|
const { exactQueues, titleOnlyQueues } = buildSearchUrlQueues(htmlEntries);
|
|
135
143
|
const apiIdentityCounts = countSearchIdentities(books.map((item) => ({
|
|
136
144
|
title: item.bookInfo?.title ?? '',
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import {
|
|
3
|
+
callGateway,
|
|
4
|
+
emptyResult,
|
|
5
|
+
formatDate,
|
|
6
|
+
formatDuration,
|
|
7
|
+
formatRating,
|
|
8
|
+
makeDeepLink,
|
|
9
|
+
requireBookId,
|
|
10
|
+
truncate,
|
|
11
|
+
} from './utils.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 3-in-1 book lookup: `/book/info` always runs; `/book/chapterinfo` and
|
|
15
|
+
* `/book/getprogress` are opt-out via `--no-chapters` / `--no-progress`
|
|
16
|
+
* so users can shave 1-2 gateway calls when only a single section is needed.
|
|
17
|
+
*
|
|
18
|
+
* Output is a flat row list with a `section` column so table/json/yaml
|
|
19
|
+
* consumers can filter without ad-hoc joins:
|
|
20
|
+
* - section=info — single row, basic metadata
|
|
21
|
+
* - section=chapter — one row per chapter (level/title/wordCount)
|
|
22
|
+
* - section=progress — single row, progress % + cumulative read time
|
|
23
|
+
*/
|
|
24
|
+
cli({
|
|
25
|
+
site: 'weread-official',
|
|
26
|
+
name: 'book',
|
|
27
|
+
access: 'read',
|
|
28
|
+
description: 'Show WeRead book metadata, chapters, and reading progress',
|
|
29
|
+
domain: 'weread.qq.com',
|
|
30
|
+
strategy: Strategy.PUBLIC,
|
|
31
|
+
browser: false,
|
|
32
|
+
args: [
|
|
33
|
+
{ name: 'bookId', positional: true, required: true, help: 'WeRead bookId (from `weread-official search`)' },
|
|
34
|
+
{ name: 'no-chapters', type: 'boolean', default: false, help: 'Skip /book/chapterinfo call' },
|
|
35
|
+
{ name: 'no-progress', type: 'boolean', default: false, help: 'Skip /book/getprogress call' },
|
|
36
|
+
],
|
|
37
|
+
columns: ['section', 'idx', 'key', 'value', 'link'],
|
|
38
|
+
func: async (args) => {
|
|
39
|
+
const bookId = requireBookId(args.bookId);
|
|
40
|
+
|
|
41
|
+
const tasks = [callGateway('/book/info', { bookId })];
|
|
42
|
+
const want = {
|
|
43
|
+
chapters: !args['no-chapters'],
|
|
44
|
+
progress: !args['no-progress'],
|
|
45
|
+
};
|
|
46
|
+
if (want.chapters) tasks.push(callGateway('/book/chapterinfo', { bookId }));
|
|
47
|
+
if (want.progress) tasks.push(callGateway('/book/getprogress', { bookId }));
|
|
48
|
+
|
|
49
|
+
const results = await Promise.all(tasks);
|
|
50
|
+
let cursor = 0;
|
|
51
|
+
const info = results[cursor++];
|
|
52
|
+
const chapters = want.chapters ? results[cursor++] : null;
|
|
53
|
+
const progress = want.progress ? results[cursor++] : null;
|
|
54
|
+
|
|
55
|
+
const rows = [];
|
|
56
|
+
|
|
57
|
+
// ── info section ────────────────────────────────────────────────────
|
|
58
|
+
const infoPairs = [
|
|
59
|
+
['bookId', String(info?.bookId ?? bookId)],
|
|
60
|
+
['title', String(info?.title ?? '')],
|
|
61
|
+
['author', String(info?.author ?? '')],
|
|
62
|
+
['translator', String(info?.translator ?? '')],
|
|
63
|
+
['category', String(info?.category ?? '')],
|
|
64
|
+
['publisher', String(info?.publisher ?? '')],
|
|
65
|
+
['publishTime', String(info?.publishTime ?? '')],
|
|
66
|
+
['isbn', String(info?.isbn ?? '')],
|
|
67
|
+
['wordCount', String(info?.wordCount ?? '')],
|
|
68
|
+
['rating', formatRating(info?.newRating)],
|
|
69
|
+
['ratingCount', String(info?.newRatingCount ?? '')],
|
|
70
|
+
['intro', truncate(info?.intro, 400)],
|
|
71
|
+
['cover', String(info?.cover ?? '')],
|
|
72
|
+
];
|
|
73
|
+
for (let i = 0; i < infoPairs.length; i += 1) {
|
|
74
|
+
const [key, value] = infoPairs[i];
|
|
75
|
+
rows.push({ section: 'info', idx: i + 1, key, value, link: '' });
|
|
76
|
+
}
|
|
77
|
+
rows.push({
|
|
78
|
+
section: 'info',
|
|
79
|
+
idx: infoPairs.length + 1,
|
|
80
|
+
key: 'link',
|
|
81
|
+
value: '',
|
|
82
|
+
link: makeDeepLink({ bookId }),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── chapters section ────────────────────────────────────────────────
|
|
86
|
+
if (chapters) {
|
|
87
|
+
const list = Array.isArray(chapters?.chapters) ? chapters.chapters : [];
|
|
88
|
+
list.forEach((ch, i) => {
|
|
89
|
+
const chapterUid = String(ch?.chapterUid ?? '').trim();
|
|
90
|
+
const level = Number(ch?.level ?? 1);
|
|
91
|
+
const indent = ' '.repeat(Math.max(0, level - 1));
|
|
92
|
+
const title = `${indent}${String(ch?.title ?? '')}`;
|
|
93
|
+
const wordCount = Number(ch?.wordCount ?? 0);
|
|
94
|
+
const paid = Number(ch?.paid ?? 0) === 1;
|
|
95
|
+
const price = Number(ch?.price ?? 0);
|
|
96
|
+
const meta = [`${wordCount}字`];
|
|
97
|
+
if (price > 0) meta.push(paid ? '已购买' : `${price}元`);
|
|
98
|
+
rows.push({
|
|
99
|
+
section: 'chapter',
|
|
100
|
+
idx: Number(ch?.chapterIdx ?? i + 1),
|
|
101
|
+
key: chapterUid,
|
|
102
|
+
value: `${title} (${meta.join(' · ')})`,
|
|
103
|
+
link: chapterUid ? makeDeepLink({ bookId, chapterUid }) : '',
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── progress section ────────────────────────────────────────────────
|
|
109
|
+
if (progress) {
|
|
110
|
+
const p = progress?.book ?? {};
|
|
111
|
+
const pct = Number(p?.progress ?? 0);
|
|
112
|
+
const updateTime = formatDate(p?.updateTime);
|
|
113
|
+
const finishTime = pct === 100 ? formatDate(p?.finishTime) : '';
|
|
114
|
+
const cumulative = formatDuration(p?.recordReadingTime);
|
|
115
|
+
const isStart = Number(p?.isStartReading ?? 0) === 1;
|
|
116
|
+
const progressPairs = [
|
|
117
|
+
['progress', `${pct}%`],
|
|
118
|
+
['cumulative', cumulative],
|
|
119
|
+
['lastReadAt', updateTime],
|
|
120
|
+
['finishedAt', finishTime],
|
|
121
|
+
['isStartReading', isStart ? 'true' : 'false'],
|
|
122
|
+
['currentChapterUid', String(p?.chapterUid ?? '')],
|
|
123
|
+
];
|
|
124
|
+
for (let i = 0; i < progressPairs.length; i += 1) {
|
|
125
|
+
const [key, value] = progressPairs[i];
|
|
126
|
+
rows.push({ section: 'progress', idx: i + 1, key, value, link: '' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (rows.length === 0) {
|
|
131
|
+
emptyResult('book', `No data returned for bookId=${bookId}`);
|
|
132
|
+
}
|
|
133
|
+
return rows;
|
|
134
|
+
},
|
|
135
|
+
});
|