@jackwener/opencli 1.7.22 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -194
- package/README.zh-CN.md +42 -260
- package/cli-manifest.json +8160 -4392
- 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/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -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/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -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/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- 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/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- 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/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -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/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- 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/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -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/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin/timeline.js +14 -7
- 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/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- 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 +231 -0
- package/clis/suno/generate.test.js +252 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +63 -0
- package/clis/suno/utils.js +549 -0
- package/clis/suno/utils.test.js +329 -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/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +13 -6
- 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/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +7 -3
- package/clis/twitter/search.test.js +41 -0
- package/clis/twitter/shared.js +155 -0
- package/clis/twitter/shared.test.js +465 -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/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +98 -11
- package/clis/weread/search.js +32 -9
- 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 +166 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
- 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 +252 -2
- package/clis/xiaohongshu/creator-notes.test.js +90 -1
- 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/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -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 +280 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +2 -19
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +17 -16
- package/clis/zhihu/collection.test.js +50 -3
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -17
- package/clis/zhihu/question.test.js +113 -11
- package/clis/zhihu/search.js +195 -43
- package/clis/zhihu/search.test.js +198 -0
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -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/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- 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 +112 -0
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- 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 +8 -0
- package/dist/src/main.js +14 -2
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- 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
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buildNotebooklmRpcBody, classifyNotebooklmPage, extractNotebooklmHistoryPreview, extractNotebooklmRpcResult, getNotebooklmPageState, normalizeNotebooklmTitle, parseNotebooklmHistoryThreadIdsResult, parseNotebooklmIdFromUrl, parseNotebooklmListResult, parseNotebooklmNoteListRawRows, parseNotebooklmNotebookDetailResult, parseNotebooklmSourceFulltextResult, parseNotebooklmSourceGuideResult, parseNotebooklmSourceListResult, } from './utils.js';
|
|
2
|
+
import { buildNotebooklmRpcBody, classifyNotebooklmPage, extractNotebooklmHistoryPreview, extractNotebooklmRpcResult, getNotebooklmPageState, isPlainObject, normalizeNotebooklmTitle, parseNotebooklmHistoryThreadIdsResult, parseNotebooklmIdFromUrl, parseNotebooklmListResult, parseNotebooklmNoteListRawRows, parseNotebooklmNotebookDetailResult, parseNotebooklmNotebookTarget, parseNotebooklmSourceFulltextResult, parseNotebooklmSourceGuideResult, parseNotebooklmSourceListResult, } from './utils.js';
|
|
3
|
+
import { CliError } from '@jackwener/opencli/errors';
|
|
3
4
|
describe('notebooklm utils', () => {
|
|
5
|
+
it('isPlainObject distinguishes objects from arrays / null / primitives', () => {
|
|
6
|
+
expect(isPlainObject({})).toBe(true);
|
|
7
|
+
expect(isPlainObject({ a: 1 })).toBe(true);
|
|
8
|
+
expect(isPlainObject([])).toBe(false);
|
|
9
|
+
expect(isPlainObject(null)).toBe(false);
|
|
10
|
+
expect(isPlainObject('x')).toBe(false);
|
|
11
|
+
expect(isPlainObject(0)).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
it('parseNotebooklmNotebookTarget accepts canonical uuid input', () => {
|
|
14
|
+
const id = '17e2b882-aaaa-bbbb-cccc-abcdef012345';
|
|
15
|
+
expect(parseNotebooklmNotebookTarget(id)).toBe(id);
|
|
16
|
+
});
|
|
17
|
+
it('parseNotebooklmNotebookTarget accepts a notebook url with uuid', () => {
|
|
18
|
+
const id = '17e2b882-aaaa-bbbb-cccc-abcdef012345';
|
|
19
|
+
expect(parseNotebooklmNotebookTarget(`https://notebooklm.google.com/notebook/${id}?pli=1`)).toBe(id);
|
|
20
|
+
});
|
|
21
|
+
it('parseNotebooklmNotebookTarget rejects non-uuid bare ids', () => {
|
|
22
|
+
expect(() => parseNotebooklmNotebookTarget('nb-demo')).toThrow(CliError);
|
|
23
|
+
});
|
|
24
|
+
it('parseNotebooklmNotebookTarget rejects malformed notebook urls', () => {
|
|
25
|
+
expect(() => parseNotebooklmNotebookTarget('https://notebooklm.google.com/notebook/not-a-uuid')).toThrow(CliError);
|
|
26
|
+
});
|
|
27
|
+
it('parseNotebooklmNotebookTarget rejects off-domain or non-canonical notebook urls', () => {
|
|
28
|
+
const id = '17e2b882-aaaa-bbbb-cccc-abcdef012345';
|
|
29
|
+
expect(() => parseNotebooklmNotebookTarget(`https://evil.test/notebook/${id}`)).toThrow(CliError);
|
|
30
|
+
expect(() => parseNotebooklmNotebookTarget(`http://notebooklm.google.com/notebook/${id}`)).toThrow(CliError);
|
|
31
|
+
expect(() => parseNotebooklmNotebookTarget(`https://notebooklm.google.com:444/notebook/${id}`)).toThrow(CliError);
|
|
32
|
+
expect(() => parseNotebooklmNotebookTarget(`https://user:notsecret@notebooklm.google.com/notebook/${id}`)).toThrow(CliError);
|
|
33
|
+
});
|
|
34
|
+
it('parseNotebooklmNotebookTarget rejects empty input', () => {
|
|
35
|
+
expect(() => parseNotebooklmNotebookTarget('')).toThrow(CliError);
|
|
36
|
+
expect(() => parseNotebooklmNotebookTarget(' ')).toThrow(CliError);
|
|
37
|
+
});
|
|
4
38
|
it('parses notebook id from a notebook url', () => {
|
|
5
39
|
expect(parseNotebooklmIdFromUrl('https://notebooklm.google.com/notebook/abc-123')).toBe('abc-123');
|
|
6
40
|
});
|
|
@@ -387,4 +421,29 @@ describe('notebooklm utils', () => {
|
|
|
387
421
|
notebookCount: 0,
|
|
388
422
|
});
|
|
389
423
|
});
|
|
424
|
+
it('reads page state through Browser Bridge evaluate envelopes', async () => {
|
|
425
|
+
const page = {
|
|
426
|
+
evaluate: async () => ({
|
|
427
|
+
session: 'site:notebooklm:abc',
|
|
428
|
+
data: {
|
|
429
|
+
url: 'https://notebooklm.google.com/notebook/nb-demo',
|
|
430
|
+
title: 'Demo Notebook - NotebookLM',
|
|
431
|
+
hostname: 'notebooklm.google.com',
|
|
432
|
+
kind: 'notebook',
|
|
433
|
+
notebookId: 'nb-demo',
|
|
434
|
+
loginRequired: false,
|
|
435
|
+
notebookCount: 0,
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
};
|
|
439
|
+
await expect(getNotebooklmPageState(page)).resolves.toEqual({
|
|
440
|
+
url: 'https://notebooklm.google.com/notebook/nb-demo',
|
|
441
|
+
title: 'Demo Notebook - NotebookLM',
|
|
442
|
+
hostname: 'notebooklm.google.com',
|
|
443
|
+
kind: 'notebook',
|
|
444
|
+
notebookId: 'nb-demo',
|
|
445
|
+
loginRequired: false,
|
|
446
|
+
notebookCount: 0,
|
|
447
|
+
});
|
|
448
|
+
});
|
|
390
449
|
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
+
import { callNotebooklmRpc } from './rpc.js';
|
|
5
|
+
import { buildNotebooklmNotebookUrl, ensureNotebooklmHome, parseNotebooklmNotebookTarget, requireNotebooklmExecute, requireNotebooklmSession } from './utils.js';
|
|
6
|
+
|
|
7
|
+
const NOTEBOOKLM_CREATE_NOTE_RPC_ID = 'CYK0Xb';
|
|
8
|
+
const NOTEBOOKLM_MUTATE_NOTE_RPC_ID = 'cYAfTb';
|
|
9
|
+
const MAX_TITLE_LEN = 200;
|
|
10
|
+
const MAX_CONTENT_LEN = 1_000_000;
|
|
11
|
+
const NOTE_UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
12
|
+
|
|
13
|
+
export function parseNoteTitle(value) {
|
|
14
|
+
const title = String(value ?? '').trim();
|
|
15
|
+
if (!title) throw new ArgumentError('--title is required');
|
|
16
|
+
if (title.length > MAX_TITLE_LEN) {
|
|
17
|
+
throw new ArgumentError(`--title must be at most ${MAX_TITLE_LEN} characters (got ${title.length})`);
|
|
18
|
+
}
|
|
19
|
+
return title;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseNoteContent(value) {
|
|
23
|
+
const content = String(value ?? '');
|
|
24
|
+
if (!content) throw new ArgumentError('--content is required');
|
|
25
|
+
if (content.length > MAX_CONTENT_LEN) {
|
|
26
|
+
throw new ArgumentError(`--content exceeds ${MAX_CONTENT_LEN} characters; split into smaller notes.`);
|
|
27
|
+
}
|
|
28
|
+
return content;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildCreateNoteShellArgs(projectId) {
|
|
32
|
+
return [projectId, '', [1], null, 'New Note', null, [2]];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildMutateNoteArgs(projectId, noteId, content, title) {
|
|
36
|
+
return [projectId, noteId, [[[content, title, [], 0]]], [2]];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toExcludedUuidSet(excludedIds) {
|
|
40
|
+
return new Set(excludedIds.map((id) => String(id ?? '').toLowerCase()).filter(Boolean));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseNoteIdFromResult(result, excludedIds = []) {
|
|
44
|
+
const excluded = toExcludedUuidSet(excludedIds);
|
|
45
|
+
if (typeof result === 'string') return NOTE_UUID_RE.test(result) && !excluded.has(result.toLowerCase()) ? result : '';
|
|
46
|
+
const stack = [result];
|
|
47
|
+
while (stack.length) {
|
|
48
|
+
const node = stack.shift();
|
|
49
|
+
if (typeof node === 'string') {
|
|
50
|
+
if (NOTE_UUID_RE.test(node) && !excluded.has(node.toLowerCase())) return node;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(node)) for (const child of node) stack.push(child);
|
|
54
|
+
else if (node && typeof node === 'object') for (const v of Object.values(node)) stack.push(v);
|
|
55
|
+
}
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cli({
|
|
60
|
+
site: NOTEBOOKLM_SITE,
|
|
61
|
+
name: 'write-note',
|
|
62
|
+
access: 'write',
|
|
63
|
+
description: 'Create a Studio note in an existing NotebookLM notebook with the given title and Markdown content',
|
|
64
|
+
domain: NOTEBOOKLM_DOMAIN,
|
|
65
|
+
strategy: Strategy.COOKIE,
|
|
66
|
+
browser: true,
|
|
67
|
+
navigateBefore: false,
|
|
68
|
+
args: [
|
|
69
|
+
{ name: 'notebook', positional: true, required: true, help: 'Notebook id from `notebooklm list` or full notebook URL' },
|
|
70
|
+
{ name: 'title', required: true, help: 'Note title (1-200 chars)' },
|
|
71
|
+
{ name: 'content', required: true, help: 'Note body as Markdown' },
|
|
72
|
+
{ name: 'execute', type: 'boolean', help: 'Actually create the remote NotebookLM note' },
|
|
73
|
+
],
|
|
74
|
+
columns: ['notebook_id', 'note_id', 'title', 'notebook_url'],
|
|
75
|
+
func: async (page, kwargs) => {
|
|
76
|
+
const notebookId = parseNotebooklmNotebookTarget(String(kwargs.notebook ?? ''));
|
|
77
|
+
const title = parseNoteTitle(kwargs.title);
|
|
78
|
+
const content = parseNoteContent(kwargs.content);
|
|
79
|
+
requireNotebooklmExecute(kwargs.execute, 'create a NotebookLM note');
|
|
80
|
+
await ensureNotebooklmHome(page);
|
|
81
|
+
await requireNotebooklmSession(page);
|
|
82
|
+
const shellRpc = await callNotebooklmRpc(page, NOTEBOOKLM_CREATE_NOTE_RPC_ID, buildCreateNoteShellArgs(notebookId));
|
|
83
|
+
const noteId = parseNoteIdFromResult(shellRpc.result, [notebookId]);
|
|
84
|
+
if (!noteId) {
|
|
85
|
+
throw new CommandExecutionError('NotebookLM CreateNote RPC returned no note id');
|
|
86
|
+
}
|
|
87
|
+
await callNotebooklmRpc(page, NOTEBOOKLM_MUTATE_NOTE_RPC_ID, buildMutateNoteArgs(notebookId, noteId, content, title));
|
|
88
|
+
return [{
|
|
89
|
+
notebook_id: notebookId,
|
|
90
|
+
note_id: noteId,
|
|
91
|
+
title,
|
|
92
|
+
notebook_url: buildNotebooklmNotebookUrl(notebookId),
|
|
93
|
+
}];
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const __test__ = {
|
|
98
|
+
parseNoteTitle,
|
|
99
|
+
parseNoteContent,
|
|
100
|
+
buildCreateNoteShellArgs,
|
|
101
|
+
buildMutateNoteArgs,
|
|
102
|
+
parseNoteIdFromResult,
|
|
103
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import { __test__ } from './write-note.js';
|
|
5
|
+
|
|
6
|
+
const { parseNoteTitle, parseNoteContent, buildCreateNoteShellArgs, buildMutateNoteArgs, parseNoteIdFromResult } = __test__;
|
|
7
|
+
|
|
8
|
+
describe('notebooklm write-note', () => {
|
|
9
|
+
it('parseNoteTitle accepts 1-200 char titles', () => {
|
|
10
|
+
expect(parseNoteTitle('Note A')).toBe('Note A');
|
|
11
|
+
expect(parseNoteTitle(' spaced ')).toBe('spaced');
|
|
12
|
+
expect(parseNoteTitle('x'.repeat(200))).toHaveLength(200);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('parseNoteTitle rejects empty / too-long titles', () => {
|
|
16
|
+
expect(() => parseNoteTitle('')).toThrow(ArgumentError);
|
|
17
|
+
expect(() => parseNoteTitle(' ')).toThrow(ArgumentError);
|
|
18
|
+
expect(() => parseNoteTitle('x'.repeat(201))).toThrow(ArgumentError);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parseNoteContent accepts non-empty content', () => {
|
|
22
|
+
expect(parseNoteContent('# heading\n\nbody')).toBe('# heading\n\nbody');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('parseNoteContent rejects empty content', () => {
|
|
26
|
+
expect(() => parseNoteContent('')).toThrow(ArgumentError);
|
|
27
|
+
expect(() => parseNoteContent(undefined)).toThrow(ArgumentError);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('buildCreateNoteShellArgs matches the HAR-verified wire format', () => {
|
|
31
|
+
expect(buildCreateNoteShellArgs('nb-123')).toEqual([
|
|
32
|
+
'nb-123', '', [1], null, 'New Note', null, [2],
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('buildMutateNoteArgs puts content before title in the inner tuple', () => {
|
|
37
|
+
expect(buildMutateNoteArgs('nb-123', 'note-7', 'body content', 'title-x')).toEqual([
|
|
38
|
+
'nb-123', 'note-7', [[['body content', 'title-x', [], 0]]], [2],
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('parseNoteIdFromResult walks the tree for the note-id UUID', () => {
|
|
43
|
+
const id = '0312fc89-075e-4b3a-810d-141fc8d5af6d';
|
|
44
|
+
expect(parseNoteIdFromResult([[[ [id] ]]])).toBe(id);
|
|
45
|
+
expect(parseNoteIdFromResult({ shell: { noteId: id } })).toBe(id);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('parseNoteIdFromResult ignores non-UUID strings', () => {
|
|
49
|
+
expect(parseNoteIdFromResult([ 'project-id', 'not-a-uuid' ])).toBe('');
|
|
50
|
+
expect(parseNoteIdFromResult(null)).toBe('');
|
|
51
|
+
expect(parseNoteIdFromResult([])).toBe('');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('parseNoteIdFromResult skips the input notebook id before selecting the created note id', () => {
|
|
55
|
+
const notebookId = '17e2b882-6a01-4c6c-9262-0738dfa2abee';
|
|
56
|
+
const noteId = '0312fc89-075e-4b3a-810d-141fc8d5af6d';
|
|
57
|
+
expect(parseNoteIdFromResult([notebookId, [[noteId]]], [notebookId])).toBe(noteId);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('refuses to create a remote note without --execute', async () => {
|
|
61
|
+
const command = getRegistry().get('notebooklm/write-note');
|
|
62
|
+
const page = { goto: vi.fn() };
|
|
63
|
+
await expect(command.func(page, {
|
|
64
|
+
notebook: '17e2b882-6a01-4c6c-9262-0738dfa2abee',
|
|
65
|
+
title: 'Draft note',
|
|
66
|
+
content: 'body',
|
|
67
|
+
})).rejects.toThrow(ArgumentError);
|
|
68
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
});
|
package/clis/pixiv/detail.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { pixivFetch } from './utils.js';
|
|
4
|
+
|
|
5
|
+
function requireIllustBody(body, id) {
|
|
6
|
+
if (!body || Array.isArray(body) || typeof body !== 'object') {
|
|
7
|
+
throw new CommandExecutionError(`Pixiv illustration ${id} returned malformed detail payload`);
|
|
8
|
+
}
|
|
9
|
+
const illustId = String(body.illustId ?? '').trim();
|
|
10
|
+
const title = String(body.illustTitle ?? '').trim();
|
|
11
|
+
const userName = String(body.userName ?? '').trim();
|
|
12
|
+
const userId = String(body.userId ?? '').trim();
|
|
13
|
+
if (!/^\d+$/.test(illustId) || illustId !== id || !title || !userName || !/^\d+$/.test(userId)) {
|
|
14
|
+
throw new CommandExecutionError(`Pixiv illustration ${id} returned malformed detail payload`);
|
|
15
|
+
}
|
|
16
|
+
return { ...body, illustId, illustTitle: title, userName, userId };
|
|
17
|
+
}
|
|
18
|
+
|
|
2
19
|
cli({
|
|
3
20
|
site: 'pixiv',
|
|
4
21
|
name: 'detail',
|
|
@@ -6,7 +23,6 @@ cli({
|
|
|
6
23
|
description: 'View illustration details (tags, stats, URLs)',
|
|
7
24
|
domain: 'www.pixiv.net',
|
|
8
25
|
strategy: Strategy.COOKIE,
|
|
9
|
-
browser: true,
|
|
10
26
|
args: [
|
|
11
27
|
{ name: 'id', required: true, positional: true, help: 'Illustration ID' },
|
|
12
28
|
],
|
|
@@ -23,37 +39,28 @@ cli({
|
|
|
23
39
|
'created',
|
|
24
40
|
'url',
|
|
25
41
|
],
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
likes: b.likeCount,
|
|
51
|
-
views: b.viewCount,
|
|
52
|
-
tags: (b.tags?.tags || []).map(t => t.tag).join(', '),
|
|
53
|
-
created: b.createDate?.split('T')[0] || '',
|
|
54
|
-
url: 'https://www.pixiv.net/artworks/' + b.illustId
|
|
55
|
-
}];
|
|
56
|
-
})()
|
|
57
|
-
` },
|
|
58
|
-
],
|
|
42
|
+
func: async (page, kwargs) => {
|
|
43
|
+
const id = String(kwargs.id ?? '');
|
|
44
|
+
if (!/^\d+$/.test(id)) {
|
|
45
|
+
throw new ArgumentError(`Invalid illustration ID: ${id}`, 'Example: opencli pixiv detail 123456');
|
|
46
|
+
}
|
|
47
|
+
const body = await pixivFetch(page, `/ajax/illust/${id}`, {
|
|
48
|
+
notFoundMsg: `Illustration not found: ${id}`,
|
|
49
|
+
});
|
|
50
|
+
const b = requireIllustBody(body, id);
|
|
51
|
+
return [{
|
|
52
|
+
illust_id: b.illustId,
|
|
53
|
+
title: b.illustTitle,
|
|
54
|
+
author: b.userName,
|
|
55
|
+
user_id: b.userId,
|
|
56
|
+
type: b.illustType === 0 ? 'illust' : b.illustType === 1 ? 'manga' : b.illustType === 2 ? 'ugoira' : String(b.illustType),
|
|
57
|
+
pages: b.pageCount,
|
|
58
|
+
bookmarks: b.bookmarkCount,
|
|
59
|
+
likes: b.likeCount,
|
|
60
|
+
views: b.viewCount,
|
|
61
|
+
tags: (b.tags?.tags || []).map(t => t.tag).join(', '),
|
|
62
|
+
created: b.createDate?.split('T')[0] || '',
|
|
63
|
+
url: `https://www.pixiv.net/artworks/${b.illustId}`,
|
|
64
|
+
}];
|
|
65
|
+
},
|
|
59
66
|
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
4
|
+
import { createPageMock } from '../test-utils.js';
|
|
5
|
+
import './detail.js';
|
|
6
|
+
let cmd;
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
cmd = getRegistry().get('pixiv/detail');
|
|
9
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
10
|
+
});
|
|
11
|
+
describe('pixiv detail', () => {
|
|
12
|
+
it('throws ArgumentError on invalid illustration ID before navigation', async () => {
|
|
13
|
+
const page = createPageMock([]);
|
|
14
|
+
await expect(cmd.func(page, { id: 'xyz' })).rejects.toThrow(ArgumentError);
|
|
15
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
16
|
+
});
|
|
17
|
+
it('throws AuthRequiredError on 401', async () => {
|
|
18
|
+
const page = createPageMock([{ __httpError: 401 }]);
|
|
19
|
+
await expect(cmd.func(page, { id: '12345' })).rejects.toThrow(AuthRequiredError);
|
|
20
|
+
});
|
|
21
|
+
it('throws CommandExecutionError on 404', async () => {
|
|
22
|
+
const page = createPageMock([{ __httpError: 404 }]);
|
|
23
|
+
await expect(cmd.func(page, { id: '12345' })).rejects.toThrow(CommandExecutionError);
|
|
24
|
+
});
|
|
25
|
+
it('throws CommandExecutionError on non-auth HTTP failure', async () => {
|
|
26
|
+
const page = createPageMock([{ __httpError: 500 }]);
|
|
27
|
+
await expect(cmd.func(page, { id: '12345' })).rejects.toThrow(CommandExecutionError);
|
|
28
|
+
});
|
|
29
|
+
it('surfaces HTTP error body messages from pixivFetch', async () => {
|
|
30
|
+
const page = createPageMock([{ __httpError: 429, message: 'Too many requests' }]);
|
|
31
|
+
await expect(cmd.func(page, { id: '12345' })).rejects.toMatchObject({
|
|
32
|
+
code: 'COMMAND_EXEC',
|
|
33
|
+
message: expect.stringContaining('Too many requests'),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('fails typed when the detail body lacks stable illustration identity fields', async () => {
|
|
37
|
+
const page = createPageMock([{ body: { illustId: '12345', illustTitle: 'Title' } }]);
|
|
38
|
+
await expect(cmd.func(page, { id: '12345' })).rejects.toMatchObject({
|
|
39
|
+
code: 'COMMAND_EXEC',
|
|
40
|
+
message: expect.stringContaining('malformed detail payload'),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it('fails typed when the detail body id does not match the requested illustration', async () => {
|
|
44
|
+
const page = createPageMock([
|
|
45
|
+
{
|
|
46
|
+
body: {
|
|
47
|
+
illustId: '99999',
|
|
48
|
+
illustTitle: 'Wrong',
|
|
49
|
+
userName: 'Artist',
|
|
50
|
+
userId: '99',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
await expect(cmd.func(page, { id: '12345' })).rejects.toMatchObject({
|
|
55
|
+
code: 'COMMAND_EXEC',
|
|
56
|
+
message: expect.stringContaining('malformed detail payload'),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
it('returns detail row with mapped fields', async () => {
|
|
60
|
+
const page = createPageMock([
|
|
61
|
+
{
|
|
62
|
+
body: {
|
|
63
|
+
illustId: '12345',
|
|
64
|
+
illustTitle: 'Test Illust',
|
|
65
|
+
userName: 'Test Artist',
|
|
66
|
+
userId: '99',
|
|
67
|
+
illustType: 1,
|
|
68
|
+
pageCount: 4,
|
|
69
|
+
bookmarkCount: 200,
|
|
70
|
+
likeCount: 100,
|
|
71
|
+
viewCount: 5000,
|
|
72
|
+
tags: { tags: [{ tag: 'original' }, { tag: 'fantasy' }] },
|
|
73
|
+
createDate: '2025-01-15T12:00:00+09:00',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
const result = await cmd.func(page, { id: '12345' });
|
|
78
|
+
expect(result).toEqual([{
|
|
79
|
+
illust_id: '12345',
|
|
80
|
+
title: 'Test Illust',
|
|
81
|
+
author: 'Test Artist',
|
|
82
|
+
user_id: '99',
|
|
83
|
+
type: 'manga',
|
|
84
|
+
pages: 4,
|
|
85
|
+
bookmarks: 200,
|
|
86
|
+
likes: 100,
|
|
87
|
+
views: 5000,
|
|
88
|
+
tags: 'original, fantasy',
|
|
89
|
+
created: '2025-01-15',
|
|
90
|
+
url: 'https://www.pixiv.net/artworks/12345',
|
|
91
|
+
}]);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/clis/pixiv/user.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { pixivFetch } from './utils.js';
|
|
4
|
+
|
|
5
|
+
function requireUserBody(body, uid) {
|
|
6
|
+
if (!body || Array.isArray(body) || typeof body !== 'object') {
|
|
7
|
+
throw new CommandExecutionError(`Pixiv user ${uid} returned malformed profile payload`);
|
|
8
|
+
}
|
|
9
|
+
const name = String(body.name ?? '').trim();
|
|
10
|
+
if (!name) {
|
|
11
|
+
throw new CommandExecutionError(`Pixiv user ${uid} returned malformed profile payload`);
|
|
12
|
+
}
|
|
13
|
+
return { ...body, name };
|
|
14
|
+
}
|
|
15
|
+
|
|
2
16
|
cli({
|
|
3
17
|
site: 'pixiv',
|
|
4
18
|
name: 'user',
|
|
@@ -6,7 +20,6 @@ cli({
|
|
|
6
20
|
description: 'View Pixiv artist profile',
|
|
7
21
|
domain: 'www.pixiv.net',
|
|
8
22
|
strategy: Strategy.COOKIE,
|
|
9
|
-
browser: true,
|
|
10
23
|
args: [
|
|
11
24
|
{ name: 'uid', required: true, positional: true, help: 'Pixiv user ID' },
|
|
12
25
|
],
|
|
@@ -21,34 +34,26 @@ cli({
|
|
|
21
34
|
'comment',
|
|
22
35
|
'url',
|
|
23
36
|
],
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
manga: typeof b.manga === 'object' ? Object.keys(b.manga).length : (b.manga || 0),
|
|
47
|
-
novels: typeof b.novels === 'object' ? Object.keys(b.novels).length : (b.novels || 0),
|
|
48
|
-
comment: (b.comment || '').slice(0, 80),
|
|
49
|
-
url: 'https://www.pixiv.net/users/' + uid
|
|
50
|
-
}];
|
|
51
|
-
})()
|
|
52
|
-
` },
|
|
53
|
-
],
|
|
37
|
+
func: async (page, kwargs) => {
|
|
38
|
+
const uid = String(kwargs.uid ?? '');
|
|
39
|
+
if (!/^\d+$/.test(uid)) {
|
|
40
|
+
throw new ArgumentError(`Invalid user ID: ${uid}`, 'Example: opencli pixiv user 123456');
|
|
41
|
+
}
|
|
42
|
+
const body = await pixivFetch(page, `/ajax/user/${uid}`, {
|
|
43
|
+
params: { full: 1 },
|
|
44
|
+
notFoundMsg: `User not found: ${uid}`,
|
|
45
|
+
});
|
|
46
|
+
const b = requireUserBody(body, uid);
|
|
47
|
+
return [{
|
|
48
|
+
user_id: uid,
|
|
49
|
+
name: b.name,
|
|
50
|
+
premium: b.premium ? 'Yes' : 'No',
|
|
51
|
+
following: b.following,
|
|
52
|
+
illusts: typeof b.illusts === 'object' ? Object.keys(b.illusts).length : (b.illusts || 0),
|
|
53
|
+
manga: typeof b.manga === 'object' ? Object.keys(b.manga).length : (b.manga || 0),
|
|
54
|
+
novels: typeof b.novels === 'object' ? Object.keys(b.novels).length : (b.novels || 0),
|
|
55
|
+
comment: (b.comment || '').slice(0, 80),
|
|
56
|
+
url: `https://www.pixiv.net/users/${uid}`,
|
|
57
|
+
}];
|
|
58
|
+
},
|
|
54
59
|
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
4
|
+
import { createPageMock } from '../test-utils.js';
|
|
5
|
+
import './user.js';
|
|
6
|
+
let cmd;
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
cmd = getRegistry().get('pixiv/user');
|
|
9
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
10
|
+
});
|
|
11
|
+
describe('pixiv user', () => {
|
|
12
|
+
it('throws ArgumentError on invalid user ID before navigation', async () => {
|
|
13
|
+
const page = createPageMock([]);
|
|
14
|
+
await expect(cmd.func(page, { uid: 'abc' })).rejects.toThrow(ArgumentError);
|
|
15
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
16
|
+
});
|
|
17
|
+
it('throws AuthRequiredError on 401', async () => {
|
|
18
|
+
const page = createPageMock([{ __httpError: 401 }]);
|
|
19
|
+
await expect(cmd.func(page, { uid: '11' })).rejects.toThrow(AuthRequiredError);
|
|
20
|
+
});
|
|
21
|
+
it('throws CommandExecutionError on 404', async () => {
|
|
22
|
+
const page = createPageMock([{ __httpError: 404 }]);
|
|
23
|
+
await expect(cmd.func(page, { uid: '11' })).rejects.toThrow(CommandExecutionError);
|
|
24
|
+
});
|
|
25
|
+
it('throws CommandExecutionError on non-auth HTTP failure', async () => {
|
|
26
|
+
const page = createPageMock([{ __httpError: 500 }]);
|
|
27
|
+
await expect(cmd.func(page, { uid: '11' })).rejects.toThrow(CommandExecutionError);
|
|
28
|
+
});
|
|
29
|
+
it('unwraps Browser Bridge envelopes around Pixiv API payloads', async () => {
|
|
30
|
+
const page = createPageMock([
|
|
31
|
+
{
|
|
32
|
+
session: 'site:pixiv',
|
|
33
|
+
data: {
|
|
34
|
+
body: {
|
|
35
|
+
name: 'Envelope Artist',
|
|
36
|
+
premium: false,
|
|
37
|
+
following: 0,
|
|
38
|
+
illusts: {},
|
|
39
|
+
manga: {},
|
|
40
|
+
novels: {},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
const result = await cmd.func(page, { uid: '12' });
|
|
46
|
+
expect(result[0]).toMatchObject({
|
|
47
|
+
user_id: '12',
|
|
48
|
+
name: 'Envelope Artist',
|
|
49
|
+
premium: 'No',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it('surfaces Pixiv API error bodies instead of treating them as not found', async () => {
|
|
53
|
+
const page = createPageMock([{ error: true, message: 'rate limited' }]);
|
|
54
|
+
await expect(cmd.func(page, { uid: '11' })).rejects.toMatchObject({
|
|
55
|
+
code: 'COMMAND_EXEC',
|
|
56
|
+
message: 'rate limited',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
it('fails typed on malformed Pixiv API payloads', async () => {
|
|
60
|
+
const page = createPageMock([{ ok: true }]);
|
|
61
|
+
await expect(cmd.func(page, { uid: '11' })).rejects.toMatchObject({
|
|
62
|
+
code: 'COMMAND_EXEC',
|
|
63
|
+
message: expect.stringContaining('malformed API payload'),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it('fails typed when the user body lacks stable profile identity fields', async () => {
|
|
67
|
+
const page = createPageMock([{ body: { premium: false, following: 0 } }]);
|
|
68
|
+
await expect(cmd.func(page, { uid: '11' })).rejects.toMatchObject({
|
|
69
|
+
code: 'COMMAND_EXEC',
|
|
70
|
+
message: expect.stringContaining('malformed profile payload'),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('returns profile row with computed counts for object-shaped illust fields', async () => {
|
|
74
|
+
const page = createPageMock([
|
|
75
|
+
{
|
|
76
|
+
body: {
|
|
77
|
+
name: 'Test Artist',
|
|
78
|
+
premium: true,
|
|
79
|
+
following: 42,
|
|
80
|
+
illusts: { '111': null, '222': null, '333': null },
|
|
81
|
+
manga: {},
|
|
82
|
+
novels: { '999': null },
|
|
83
|
+
comment: 'Hello world',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
const result = await cmd.func(page, { uid: '11' });
|
|
88
|
+
expect(result).toEqual([{
|
|
89
|
+
user_id: '11',
|
|
90
|
+
name: 'Test Artist',
|
|
91
|
+
premium: 'Yes',
|
|
92
|
+
following: 42,
|
|
93
|
+
illusts: 3,
|
|
94
|
+
manga: 0,
|
|
95
|
+
novels: 1,
|
|
96
|
+
comment: 'Hello world',
|
|
97
|
+
url: 'https://www.pixiv.net/users/11',
|
|
98
|
+
}]);
|
|
99
|
+
});
|
|
100
|
+
});
|