@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
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
|
|
4
|
+
const ALLOWED_HOSTS = new Set([
|
|
5
|
+
'share.note.youdao.com',
|
|
6
|
+
'note.youdao.com',
|
|
7
|
+
'share.note.youdao.cn',
|
|
8
|
+
'note.youdao.cn',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function unwrapEvaluateResult(payload) {
|
|
12
|
+
if (payload && !Array.isArray(payload) && typeof payload === 'object' && 'session' in payload && 'data' in payload) {
|
|
13
|
+
return payload.data;
|
|
14
|
+
}
|
|
15
|
+
return payload;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeShareUrl(raw) {
|
|
19
|
+
const value = String(raw ?? '').trim();
|
|
20
|
+
if (!value) {
|
|
21
|
+
throw new ArgumentError('youdao note url cannot be empty', 'Pass a full public share URL from Youdao Notes.');
|
|
22
|
+
}
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = new URL(value);
|
|
26
|
+
} catch {
|
|
27
|
+
throw new ArgumentError('Invalid Youdao Note URL', 'Example: https://share.note.youdao.com/ynoteshare/index.html?id=...&type=note');
|
|
28
|
+
}
|
|
29
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
30
|
+
throw new ArgumentError('Youdao Note URL must use http or https');
|
|
31
|
+
}
|
|
32
|
+
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
|
|
33
|
+
throw new ArgumentError('Youdao Note URL must be under note.youdao.com or note.youdao.cn');
|
|
34
|
+
}
|
|
35
|
+
if (!parsed.searchParams.get('id')) {
|
|
36
|
+
throw new ArgumentError('Youdao Note URL must include an id query parameter');
|
|
37
|
+
}
|
|
38
|
+
const type = parsed.searchParams.get('type');
|
|
39
|
+
if (type && type !== 'note') {
|
|
40
|
+
throw new ArgumentError('youdao note only accepts shared note URLs', 'Shared notebooks are not implemented yet.');
|
|
41
|
+
}
|
|
42
|
+
return parsed.toString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatYoudaoTimestamp(value) {
|
|
46
|
+
if (value == null || value === '') return '';
|
|
47
|
+
const numeric = Number(value);
|
|
48
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return String(value);
|
|
49
|
+
const millis = numeric < 10_000_000_000 ? numeric * 1000 : numeric;
|
|
50
|
+
const date = new Date(millis);
|
|
51
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
52
|
+
return date.toISOString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildExtractorJs() {
|
|
56
|
+
const walkTextFn = `
|
|
57
|
+
function walkText(node, out) {
|
|
58
|
+
if (!node || typeof node !== 'object') return;
|
|
59
|
+
if (Array.isArray(node)) {
|
|
60
|
+
for (var i = 0; i < node.length; i += 1) walkText(node[i], out);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (typeof node[8] === 'string') {
|
|
64
|
+
var text = node[8].replace(/\\s+/g, ' ').trim();
|
|
65
|
+
if (text) out.push(text);
|
|
66
|
+
}
|
|
67
|
+
var keys = Object.keys(node);
|
|
68
|
+
for (var j = 0; j < keys.length; j += 1) {
|
|
69
|
+
var value = node[keys[j]];
|
|
70
|
+
if (value && typeof value === 'object') walkText(value, out);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
return `
|
|
75
|
+
(function() {
|
|
76
|
+
${walkTextFn}
|
|
77
|
+
function cleanText(value) {
|
|
78
|
+
return String(value || '').replace(/\\u00a0/g, ' ').replace(/[ \\t]+\\n/g, '\\n').replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
79
|
+
}
|
|
80
|
+
function pageText() {
|
|
81
|
+
return cleanText((document.body && (document.body.innerText || document.body.textContent)) || '').slice(0, 1000);
|
|
82
|
+
}
|
|
83
|
+
function classifyBodyText(text) {
|
|
84
|
+
if (/登录|登陆|请先登录|无权|权限|访问受限|验证码|安全验证|login|forbidden|permission/i.test(text)) return 'auth';
|
|
85
|
+
if (/分享已取消|分享不存在|文件不存在|笔记不存在|页面不存在|已过期|不存在|not found|404/i.test(text)) return 'not_found';
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
function findStoreState(value, depth, seen) {
|
|
89
|
+
if (!value || typeof value !== 'object' || depth > 10) return null;
|
|
90
|
+
if (seen.indexOf(value) !== -1) return null;
|
|
91
|
+
seen.push(value);
|
|
92
|
+
if (value.storeState && typeof value.storeState === 'object') return value.storeState;
|
|
93
|
+
if (value.content && value.content.data && typeof value.content.data === 'object') return value;
|
|
94
|
+
var keys = Object.keys(value);
|
|
95
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
96
|
+
var found = findStoreState(value[keys[i]], depth + 1, seen);
|
|
97
|
+
if (found) return found;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function findStoreFromFiber(fiber) {
|
|
102
|
+
var cursor = fiber;
|
|
103
|
+
var stack = [];
|
|
104
|
+
while (cursor || stack.length) {
|
|
105
|
+
if (!cursor) {
|
|
106
|
+
cursor = stack.pop();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
var fromState = findStoreState(cursor.memoizedState, 0, []);
|
|
110
|
+
if (fromState) return fromState;
|
|
111
|
+
var fromProps = findStoreState(cursor.memoizedProps, 0, []);
|
|
112
|
+
if (fromProps) return fromProps;
|
|
113
|
+
if (cursor.sibling) stack.push(cursor.sibling);
|
|
114
|
+
cursor = cursor.child;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
var root = document.querySelector('#root');
|
|
119
|
+
var body = pageText();
|
|
120
|
+
var bodyKind = classifyBodyText(body);
|
|
121
|
+
if (!root) {
|
|
122
|
+
return [false, bodyKind || 'root_missing', body];
|
|
123
|
+
}
|
|
124
|
+
var reactKey = Object.keys(root).find(function(key) { return key.indexOf('__reactContainer$') === 0; });
|
|
125
|
+
var fiber = (root._reactRootContainer && root._reactRootContainer._internalRoot && root._reactRootContainer._internalRoot.current)
|
|
126
|
+
|| (reactKey ? root[reactKey] : null);
|
|
127
|
+
if (!fiber) {
|
|
128
|
+
return [false, bodyKind || 'react_root_missing', body];
|
|
129
|
+
}
|
|
130
|
+
var store = findStoreFromFiber(fiber);
|
|
131
|
+
if (!store) {
|
|
132
|
+
return [false, bodyKind || 'store_missing', body];
|
|
133
|
+
}
|
|
134
|
+
var contentData = store.content && store.content.data;
|
|
135
|
+
if (!contentData || typeof contentData !== 'object') {
|
|
136
|
+
return [false, bodyKind || 'content_data_missing', body];
|
|
137
|
+
}
|
|
138
|
+
var title = cleanText(contentData.tl || document.querySelector('.file-name')?.textContent || document.title || '');
|
|
139
|
+
var hasContentField = Object.prototype.hasOwnProperty.call(contentData, 'content');
|
|
140
|
+
var rawContent = hasContentField ? String(contentData.content || '') : '';
|
|
141
|
+
var content = '';
|
|
142
|
+
if (rawContent) {
|
|
143
|
+
try {
|
|
144
|
+
var parsed = JSON.parse(rawContent);
|
|
145
|
+
var parts = [];
|
|
146
|
+
walkText(parsed, parts);
|
|
147
|
+
content = cleanText(parts.join('\\n'));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
content = cleanText(rawContent);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var summary = '';
|
|
153
|
+
var keywords = [];
|
|
154
|
+
var ai = store.aiSummary;
|
|
155
|
+
if (ai && ai.aiSummary) {
|
|
156
|
+
try {
|
|
157
|
+
var aiPayload = JSON.parse(ai.aiSummary);
|
|
158
|
+
summary = cleanText(aiPayload.description || '');
|
|
159
|
+
if (Array.isArray(aiPayload.keywords)) {
|
|
160
|
+
for (var i = 0; i < aiPayload.keywords.length; i += 1) {
|
|
161
|
+
var keyword = aiPayload.keywords[i];
|
|
162
|
+
if (keyword && keyword.title) keywords.push(cleanText(((keyword.emoji || '') + ' ' + keyword.title).trim()));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
return [true, title, content, summary, keywords.join(' | '), contentData.ct || null, contentData.sz || null, hasContentField, rawContent.length, window.location.href];
|
|
168
|
+
})()
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeExtractionResult(payload, sourceUrl) {
|
|
173
|
+
const data = unwrapEvaluateResult(payload);
|
|
174
|
+
if (!Array.isArray(data)) {
|
|
175
|
+
throw new CommandExecutionError('Youdao note extractor returned a malformed payload');
|
|
176
|
+
}
|
|
177
|
+
const ok = data[0] === true;
|
|
178
|
+
if (!ok) {
|
|
179
|
+
const reason = typeof data[1] === 'string' && data[1].trim() ? data[1].trim() : 'unknown_parser_failure';
|
|
180
|
+
if (reason === 'auth') {
|
|
181
|
+
throw new AuthRequiredError('note.youdao.com', 'Youdao shared note requires login or additional permission');
|
|
182
|
+
}
|
|
183
|
+
if (reason === 'not_found') {
|
|
184
|
+
throw new EmptyResultError('youdao note', 'The shared note is missing, expired, cancelled, or inaccessible.');
|
|
185
|
+
}
|
|
186
|
+
throw new CommandExecutionError(`Youdao note parser failed: ${reason}`);
|
|
187
|
+
}
|
|
188
|
+
const title = String(data[1] ?? '');
|
|
189
|
+
const content = String(data[2] ?? '');
|
|
190
|
+
const summary = String(data[3] ?? '');
|
|
191
|
+
const keywords = String(data[4] ?? '');
|
|
192
|
+
const createTime = data[5];
|
|
193
|
+
const fileSize = data[6];
|
|
194
|
+
const hasContentField = data[7] === true;
|
|
195
|
+
const rawContentLength = Number(data[8] ?? 0);
|
|
196
|
+
const finalUrl = String(data[9] || sourceUrl);
|
|
197
|
+
if (!title) {
|
|
198
|
+
throw new CommandExecutionError('Youdao note parser did not extract a title');
|
|
199
|
+
}
|
|
200
|
+
if (!hasContentField) {
|
|
201
|
+
throw new CommandExecutionError('Youdao note parser did not find full note content in the page store');
|
|
202
|
+
}
|
|
203
|
+
if (rawContentLength > 0 && !content) {
|
|
204
|
+
throw new CommandExecutionError('Youdao note parser found note content but extracted no readable text');
|
|
205
|
+
}
|
|
206
|
+
const row = {};
|
|
207
|
+
row.title = title;
|
|
208
|
+
row.content = content;
|
|
209
|
+
row.summary = summary;
|
|
210
|
+
row.keywords = keywords;
|
|
211
|
+
row.created_at = formatYoudaoTimestamp(createTime);
|
|
212
|
+
row.file_size = fileSize == null ? '' : String(fileSize);
|
|
213
|
+
row.url = finalUrl;
|
|
214
|
+
return row;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var command = cli({
|
|
218
|
+
site: 'youdao',
|
|
219
|
+
name: 'note',
|
|
220
|
+
access: 'read',
|
|
221
|
+
description: 'Read a public shared Youdao Note',
|
|
222
|
+
domain: 'share.note.youdao.com',
|
|
223
|
+
strategy: Strategy.PUBLIC,
|
|
224
|
+
browser: true,
|
|
225
|
+
args: [
|
|
226
|
+
{ name: 'url', positional: true, required: true, help: 'Full share URL of the Youdao Note' },
|
|
227
|
+
],
|
|
228
|
+
columns: ['title', 'content', 'summary', 'keywords', 'created_at', 'file_size', 'url'],
|
|
229
|
+
func: async function(page, kwargs) {
|
|
230
|
+
const url = normalizeShareUrl(kwargs.url);
|
|
231
|
+
try {
|
|
232
|
+
await page.goto(url);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw new CommandExecutionError(`Failed to open Youdao Note URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await page.wait({ selector: '#root, .file-name, body', timeout: 10 });
|
|
238
|
+
} catch {
|
|
239
|
+
await page.wait(3).catch(function() {});
|
|
240
|
+
}
|
|
241
|
+
await page.wait(2).catch(function() {});
|
|
242
|
+
let payload;
|
|
243
|
+
try {
|
|
244
|
+
payload = await page.evaluate(buildExtractorJs());
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw new CommandExecutionError(`Youdao note extractor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
|
+
}
|
|
248
|
+
return [normalizeExtractionResult(payload, url)];
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
export var __test__ = {
|
|
253
|
+
buildExtractorJs: buildExtractorJs,
|
|
254
|
+
command: command,
|
|
255
|
+
formatYoudaoTimestamp: formatYoudaoTimestamp,
|
|
256
|
+
normalizeExtractionResult: normalizeExtractionResult,
|
|
257
|
+
normalizeShareUrl: normalizeShareUrl,
|
|
258
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
|
|
4
|
+
const { __test__ } = await import('./note.js');
|
|
5
|
+
const command = __test__.command;
|
|
6
|
+
|
|
7
|
+
function makePage(evaluatePayload) {
|
|
8
|
+
return {
|
|
9
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
evaluate: vi.fn().mockResolvedValue(evaluatePayload),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('youdao note', () => {
|
|
16
|
+
it('registers as a public browser read command', () => {
|
|
17
|
+
expect(command).toBeDefined();
|
|
18
|
+
expect(command.site).toBe('youdao');
|
|
19
|
+
expect(command.name).toBe('note');
|
|
20
|
+
expect(command.access).toBe('read');
|
|
21
|
+
expect(command.browser).toBe(true);
|
|
22
|
+
expect(command.strategy).toBe('public');
|
|
23
|
+
expect(command.domain).toBe('share.note.youdao.com');
|
|
24
|
+
expect(command.columns).toEqual(['title', 'content', 'summary', 'keywords', 'created_at', 'file_size', 'url']);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('strictly validates share URL before navigation', async () => {
|
|
28
|
+
const page = makePage({});
|
|
29
|
+
await expect(command.func(page, { url: '' })).rejects.toBeInstanceOf(ArgumentError);
|
|
30
|
+
await expect(command.func(page, { url: 'https://share.note.youdao.com.evil/ynoteshare/index.html?id=1&type=note' })).rejects.toBeInstanceOf(ArgumentError);
|
|
31
|
+
await expect(command.func(page, { url: 'javascript:alert(1)' })).rejects.toBeInstanceOf(ArgumentError);
|
|
32
|
+
await expect(command.func(page, { url: 'https://share.note.youdao.com/ynoteshare/index.html?type=note' })).rejects.toBeInstanceOf(ArgumentError);
|
|
33
|
+
await expect(command.func(page, { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=1&type=notebook' })).rejects.toBeInstanceOf(ArgumentError);
|
|
34
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('accepts canonical youdao share URLs', () => {
|
|
38
|
+
expect(__test__.normalizeShareUrl('https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note#/')).toBe('https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note#/');
|
|
39
|
+
expect(__test__.normalizeShareUrl('https://note.youdao.cn/ynoteshare/index.html?id=abc')).toBe('https://note.youdao.cn/ynoteshare/index.html?id=abc');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('unwraps Browser Bridge envelopes and returns full note rows', async () => {
|
|
43
|
+
const page = makePage({
|
|
44
|
+
session: 'site:youdao:test',
|
|
45
|
+
data: [true, 'Expert call', 'Question\nAnswer', 'Short summary', 'PCB | ABF', 1715750400000, 1234, true, 42, 'https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note#/'],
|
|
46
|
+
});
|
|
47
|
+
await expect(command.func(page, { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note#/' }))
|
|
48
|
+
.resolves.toEqual([{
|
|
49
|
+
title: 'Expert call',
|
|
50
|
+
content: 'Question\nAnswer',
|
|
51
|
+
summary: 'Short summary',
|
|
52
|
+
keywords: 'PCB | ABF',
|
|
53
|
+
created_at: '2024-05-15T05:20:00.000Z',
|
|
54
|
+
file_size: '1234',
|
|
55
|
+
url: 'https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note#/',
|
|
56
|
+
}]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('maps missing or expired shares to EmptyResultError', async () => {
|
|
60
|
+
const page = makePage([false, 'not_found']);
|
|
61
|
+
await expect(command.func(page, { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=missing&type=note' }))
|
|
62
|
+
.rejects.toBeInstanceOf(EmptyResultError);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('maps permission/login pages to AuthRequiredError', async () => {
|
|
66
|
+
const page = makePage([false, 'auth']);
|
|
67
|
+
await expect(command.func(page, { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=private&type=note' }))
|
|
68
|
+
.rejects.toBeInstanceOf(AuthRequiredError);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('treats parser drift as CommandExecutionError instead of an empty success row', async () => {
|
|
72
|
+
await expect(command.func(makePage([false, 'store_missing']), { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note' }))
|
|
73
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
74
|
+
await expect(command.func(makePage([true, 'Only title', '', '', '', null, null, false, 0, '']), { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note' }))
|
|
75
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
76
|
+
await expect(command.func(makePage([true, 'Bad body', '', '', '', null, null, true, 100, '']), { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note' }))
|
|
77
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('wraps navigation and evaluate failures as CommandExecutionError', async () => {
|
|
81
|
+
const navPage = makePage({});
|
|
82
|
+
navPage.goto.mockRejectedValueOnce(new Error('network down'));
|
|
83
|
+
await expect(command.func(navPage, { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note' }))
|
|
84
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
85
|
+
|
|
86
|
+
const evalPage = makePage({});
|
|
87
|
+
evalPage.evaluate.mockRejectedValueOnce(new Error('bad script'));
|
|
88
|
+
await expect(command.func(evalPage, { url: 'https://share.note.youdao.com/ynoteshare/index.html?id=abc&type=note' }))
|
|
89
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('keeps the full-content React store extractor contract in source', () => {
|
|
93
|
+
const js = __test__.buildExtractorJs();
|
|
94
|
+
expect(js).toContain('store.content');
|
|
95
|
+
expect(js).toContain('contentData.content');
|
|
96
|
+
expect(js).toContain('__reactContainer$');
|
|
97
|
+
expect(js).toContain('walkText');
|
|
98
|
+
});
|
|
99
|
+
});
|