@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
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* Requires: logged into creator.xiaohongshu.com in Chrome.
|
|
9
9
|
*/
|
|
10
10
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
11
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
11
12
|
const DATE_LINE_RE = /^发布于 (\d{4}年\d{2}月\d{2}日 \d{2}:\d{2})$/;
|
|
12
13
|
const METRIC_LINE_RE = /^\d+$/;
|
|
13
14
|
const VISIBILITY_LINE_RE = /可见$/;
|
|
14
15
|
const NOTE_ANALYZE_API_PATH = '/api/galaxy/creator/datacenter/note/analyze/list';
|
|
16
|
+
const NOTE_ANALYZE_PAGE_SIZE = 10;
|
|
17
|
+
const CAPTURE_POLL_ATTEMPTS = 20;
|
|
18
|
+
const CAPTURE_POLL_INTERVAL_S = 0.5;
|
|
15
19
|
const NOTE_DETAIL_PAGE_URL = 'https://creator.xiaohongshu.com/statistics/note-detail';
|
|
16
20
|
const NOTE_ID_HTML_RE = /"noteId":"([0-9a-f]{24})"/g;
|
|
17
21
|
function buildNoteDetailUrl(noteId) {
|
|
@@ -104,6 +108,237 @@ function mapAnalyzeItems(items) {
|
|
|
104
108
|
url: buildNoteDetailUrl(item.id),
|
|
105
109
|
}));
|
|
106
110
|
}
|
|
111
|
+
function unwrapEvaluateResult(payload) {
|
|
112
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload) && 'session' in payload && 'data' in payload) {
|
|
113
|
+
return payload.data;
|
|
114
|
+
}
|
|
115
|
+
return payload;
|
|
116
|
+
}
|
|
117
|
+
// Capture the dashboard's signed /api/galaxy/* responses on window.__xhsCapture
|
|
118
|
+
// since a direct fetch() from page.evaluate bypasses the x-s signing and gets 406.
|
|
119
|
+
async function installXhsFetchCaptureHook(page) {
|
|
120
|
+
await page.evaluate(`(() => {
|
|
121
|
+
window.__xhsCapture = {};
|
|
122
|
+
if (window.__xhsCaptureInstalled) return;
|
|
123
|
+
window.__xhsCaptureInstalled = true;
|
|
124
|
+
const origFetch = window.fetch;
|
|
125
|
+
window.fetch = async function(...args) {
|
|
126
|
+
const resp = await origFetch.apply(this, args);
|
|
127
|
+
try {
|
|
128
|
+
const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
|
|
129
|
+
if (url.includes('/api/galaxy/')) {
|
|
130
|
+
resp.clone().text().then((body) => {
|
|
131
|
+
try { window.__xhsCapture[url] = { status: resp.status, ok: resp.ok, body }; } catch (_) {}
|
|
132
|
+
}).catch(() => {});
|
|
133
|
+
}
|
|
134
|
+
} catch (_) {}
|
|
135
|
+
return resp;
|
|
136
|
+
};
|
|
137
|
+
const OrigXHR = window.XMLHttpRequest;
|
|
138
|
+
function HookedXHR() {
|
|
139
|
+
const xhr = new OrigXHR();
|
|
140
|
+
const origOpen = xhr.open;
|
|
141
|
+
let capturedUrl = '';
|
|
142
|
+
xhr.open = function(method, url, ...rest) {
|
|
143
|
+
capturedUrl = url;
|
|
144
|
+
return origOpen.call(this, method, url, ...rest);
|
|
145
|
+
};
|
|
146
|
+
xhr.addEventListener('load', () => {
|
|
147
|
+
try {
|
|
148
|
+
if (capturedUrl.includes('/api/galaxy/')) {
|
|
149
|
+
window.__xhsCapture[capturedUrl] = { status: xhr.status, ok: xhr.status >= 200 && xhr.status < 300, body: xhr.responseText };
|
|
150
|
+
}
|
|
151
|
+
} catch (_) {}
|
|
152
|
+
});
|
|
153
|
+
return xhr;
|
|
154
|
+
}
|
|
155
|
+
HookedXHR.prototype = OrigXHR.prototype;
|
|
156
|
+
for (const key of ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE']) {
|
|
157
|
+
if (key in OrigXHR) HookedXHR[key] = OrigXHR[key];
|
|
158
|
+
}
|
|
159
|
+
window.XMLHttpRequest = HookedXHR;
|
|
160
|
+
})()`);
|
|
161
|
+
}
|
|
162
|
+
function parseCaptureMapPayload(raw) {
|
|
163
|
+
const payload = unwrapEvaluateResult(raw);
|
|
164
|
+
if (typeof payload === 'string') {
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(payload);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
173
|
+
return payload;
|
|
174
|
+
}
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
function getAnalyzeListPageNumber(url) {
|
|
178
|
+
try {
|
|
179
|
+
const parsed = new URL(url, 'https://creator.xiaohongshu.com');
|
|
180
|
+
const pageNum = Number.parseInt(parsed.searchParams.get('page_num') || '', 10);
|
|
181
|
+
if (Number.isFinite(pageNum) && pageNum > 0)
|
|
182
|
+
return pageNum;
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
const match = String(url || '').match(/[?&]page_num=(\d+)/);
|
|
186
|
+
const pageNum = Number.parseInt(match?.[1] || '', 10);
|
|
187
|
+
return Number.isFinite(pageNum) && pageNum > 0 ? pageNum : Number.MAX_SAFE_INTEGER;
|
|
188
|
+
}
|
|
189
|
+
function harvestAnalyzeListCaptures(captureMap) {
|
|
190
|
+
const items = [];
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
let total = 0;
|
|
193
|
+
const entries = Object.entries(captureMap)
|
|
194
|
+
.filter(([url]) => url.includes('/note/analyze/list'))
|
|
195
|
+
.sort(([a], [b]) => getAnalyzeListPageNumber(a) - getAnalyzeListPageNumber(b));
|
|
196
|
+
for (const [url, capture] of entries) {
|
|
197
|
+
if (!capture?.ok) continue;
|
|
198
|
+
try {
|
|
199
|
+
const json = JSON.parse(capture.body);
|
|
200
|
+
const data = json?.data ?? {};
|
|
201
|
+
if (typeof data.total === 'number' && data.total > total) total = data.total;
|
|
202
|
+
for (const note of data.note_infos ?? []) {
|
|
203
|
+
if (!note?.id || seen.has(note.id)) continue;
|
|
204
|
+
seen.add(note.id);
|
|
205
|
+
items.push(note);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch { }
|
|
209
|
+
}
|
|
210
|
+
return { items, total };
|
|
211
|
+
}
|
|
212
|
+
function isAnalyzeCaptureComplete(items, total, limit) {
|
|
213
|
+
if (total <= 0)
|
|
214
|
+
return true;
|
|
215
|
+
return items.length >= Math.min(total, limit);
|
|
216
|
+
}
|
|
217
|
+
async function pollCaptureMap(page) {
|
|
218
|
+
let captureMap = {};
|
|
219
|
+
for (let i = 0; i < CAPTURE_POLL_ATTEMPTS; i++) {
|
|
220
|
+
await page.wait(CAPTURE_POLL_INTERVAL_S);
|
|
221
|
+
const raw = await page.evaluate('JSON.stringify(window.__xhsCapture || {})');
|
|
222
|
+
captureMap = parseCaptureMapPayload(raw);
|
|
223
|
+
if (Object.keys(captureMap).some((url) => url.includes('/note/analyze/list'))) break;
|
|
224
|
+
}
|
|
225
|
+
return captureMap;
|
|
226
|
+
}
|
|
227
|
+
// Fresh-published notes return title: "" from /note/analyze/list. Scrape the
|
|
228
|
+
// /new/note-manager card DOM (under its "全部笔记" tab, which surfaces every
|
|
229
|
+
// state including 审核中) so the rows the API leaves empty still get the
|
|
230
|
+
// derived title that the note-manager UI shows.
|
|
231
|
+
async function fetchNoteManagerTitleMap(page, neededCount) {
|
|
232
|
+
const map = new Map();
|
|
233
|
+
const scrapeCards = async () => {
|
|
234
|
+
const cards = unwrapEvaluateResult(await page.evaluate(`() => {
|
|
235
|
+
const noteIdRe = /"noteId":"([0-9a-f]{24})"/;
|
|
236
|
+
return Array.from(document.querySelectorAll('div.note[data-impression], div.note')).map((card) => {
|
|
237
|
+
const impression = card.getAttribute('data-impression') || '';
|
|
238
|
+
const id = impression.match(noteIdRe)?.[1] || '';
|
|
239
|
+
const title = (card.querySelector('.title, .raw')?.innerText || '').trim();
|
|
240
|
+
return { id, title };
|
|
241
|
+
}).filter((entry) => entry.id && entry.title);
|
|
242
|
+
}`));
|
|
243
|
+
for (const card of Array.isArray(cards) ? cards : []) {
|
|
244
|
+
if (!map.has(card.id)) map.set(card.id, card.title);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
// Scroll the first scrollable ancestor of a note card to the bottom so
|
|
248
|
+
// the list lazy-loads the rest of its rows. Page-level scrollTo does not
|
|
249
|
+
// work because the cards live inside an inner overflow-auto container.
|
|
250
|
+
const scrollInnerListToBottom = async () => {
|
|
251
|
+
return unwrapEvaluateResult(await page.evaluate(`(() => {
|
|
252
|
+
const firstCard = document.querySelector('div.note[data-impression]');
|
|
253
|
+
let el = firstCard && firstCard.parentElement;
|
|
254
|
+
while (el) {
|
|
255
|
+
const s = window.getComputedStyle(el);
|
|
256
|
+
if ((s.overflowY === 'auto' || s.overflowY === 'scroll') && el.scrollHeight > el.clientHeight + 10) {
|
|
257
|
+
el.scrollTop = el.scrollHeight;
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
el = el.parentElement;
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
})()`));
|
|
264
|
+
};
|
|
265
|
+
try {
|
|
266
|
+
await page.goto('https://creator.xiaohongshu.com/new/note-manager');
|
|
267
|
+
// Poll for the initial hydration batch and then scroll the inner list
|
|
268
|
+
// container to surface the rest of the rows. The all-notes tab is the
|
|
269
|
+
// default state so no tab click is needed here.
|
|
270
|
+
for (let i = 0; i < 12; i++) {
|
|
271
|
+
await page.wait(1);
|
|
272
|
+
await scrapeCards();
|
|
273
|
+
if (map.size >= neededCount) return map;
|
|
274
|
+
await scrollInnerListToBottom();
|
|
275
|
+
}
|
|
276
|
+
return map;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return map;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function fetchCreatorNotesByCapture(page, limit) {
|
|
283
|
+
// Land on dashboard root before installing the hook so the data-analysis
|
|
284
|
+
// SPA navigation fires page_num=1's signed request UNDER the hook.
|
|
285
|
+
await page.goto('https://creator.xiaohongshu.com/statistics');
|
|
286
|
+
await installXhsFetchCaptureHook(page);
|
|
287
|
+
await page.evaluate(`(() => {
|
|
288
|
+
history.pushState({}, '', '/statistics/data-analysis?source=official');
|
|
289
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
290
|
+
})()`);
|
|
291
|
+
let captureMap = await pollCaptureMap(page);
|
|
292
|
+
let { items, total } = harvestAnalyzeListCaptures(captureMap);
|
|
293
|
+
if (items.length === 0) return [];
|
|
294
|
+
const totalPages = total > 0 ? Math.ceil(total / NOTE_ANALYZE_PAGE_SIZE) : 1;
|
|
295
|
+
const neededPages = Math.min(totalPages, Math.ceil(limit / NOTE_ANALYZE_PAGE_SIZE));
|
|
296
|
+
for (let pageNum = 2; pageNum <= neededPages && items.length < limit; pageNum++) {
|
|
297
|
+
const clicked = unwrapEvaluateResult(await page.evaluate(`(() => {
|
|
298
|
+
const target = String(${pageNum});
|
|
299
|
+
// .d-pagination-page renders the page number doubled (a visible span +
|
|
300
|
+
// an accessibility span), so textContent for page 2 reads "22". Match
|
|
301
|
+
// both the raw digit and the doubled form to tolerate either render.
|
|
302
|
+
const btns = Array.from(document.querySelectorAll('.d-pagination-page'));
|
|
303
|
+
const match = btns.find((btn) => {
|
|
304
|
+
const text = (btn.textContent || '').trim();
|
|
305
|
+
return text === target || text === target + target;
|
|
306
|
+
});
|
|
307
|
+
if (match) { match.click(); return true; }
|
|
308
|
+
return false;
|
|
309
|
+
})()`));
|
|
310
|
+
if (!clicked) break;
|
|
311
|
+
const before = items.length;
|
|
312
|
+
let advanced = false;
|
|
313
|
+
for (let attempt = 0; attempt < CAPTURE_POLL_ATTEMPTS; attempt++) {
|
|
314
|
+
await page.wait(CAPTURE_POLL_INTERVAL_S);
|
|
315
|
+
const raw = await page.evaluate('JSON.stringify(window.__xhsCapture || {})');
|
|
316
|
+
captureMap = parseCaptureMapPayload(raw);
|
|
317
|
+
const harvested = harvestAnalyzeListCaptures(captureMap);
|
|
318
|
+
if (harvested.items.length > before) {
|
|
319
|
+
items = harvested.items;
|
|
320
|
+
total = Math.max(total, harvested.total);
|
|
321
|
+
advanced = true;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!advanced) break;
|
|
326
|
+
}
|
|
327
|
+
if (!isAnalyzeCaptureComplete(items, total, limit)) {
|
|
328
|
+
throw new CommandExecutionError(`xiaohongshu creator-notes: captured ${items.length} of ${Math.min(total, limit)} expected analyze rows; refusing partial results`);
|
|
329
|
+
}
|
|
330
|
+
const notes = mapAnalyzeItems(items).slice(0, limit);
|
|
331
|
+
const missingTitles = notes.filter((note) => !note.title).length;
|
|
332
|
+
if (missingTitles > 0) {
|
|
333
|
+
const titleMap = await fetchNoteManagerTitleMap(page, notes.length);
|
|
334
|
+
for (const note of notes) {
|
|
335
|
+
if (!note.title && note.id && titleMap.has(note.id)) {
|
|
336
|
+
note.title = titleMap.get(note.id);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return notes;
|
|
341
|
+
}
|
|
107
342
|
async function fetchCreatorNotesByApi(page, limit) {
|
|
108
343
|
const pageSize = Math.min(Math.max(limit, 10), 20);
|
|
109
344
|
const maxPages = Math.max(1, Math.ceil(limit / pageSize));
|
|
@@ -147,7 +382,16 @@ async function fetchCreatorNotesByApi(page, limit) {
|
|
|
147
382
|
return notes.slice(0, limit);
|
|
148
383
|
}
|
|
149
384
|
export async function fetchCreatorNotes(page, limit) {
|
|
150
|
-
let notes =
|
|
385
|
+
let notes = [];
|
|
386
|
+
try {
|
|
387
|
+
notes = await fetchCreatorNotesByCapture(page, limit);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
if (error instanceof CommandExecutionError) throw error;
|
|
391
|
+
}
|
|
392
|
+
if (notes.length === 0) {
|
|
393
|
+
notes = await fetchCreatorNotesByApi(page, limit);
|
|
394
|
+
}
|
|
151
395
|
if (notes.length === 0) {
|
|
152
396
|
await page.goto('https://creator.xiaohongshu.com/new/note-manager');
|
|
153
397
|
const maxPageDowns = Math.max(0, Math.ceil(limit / 10) + 1);
|
|
@@ -210,7 +454,7 @@ cli({
|
|
|
210
454
|
const limit = kwargs.limit || 20;
|
|
211
455
|
const notes = await fetchCreatorNotes(page, limit);
|
|
212
456
|
if (!Array.isArray(notes) || notes.length === 0) {
|
|
213
|
-
throw new
|
|
457
|
+
throw new EmptyResultError('xiaohongshu creator-notes', 'No notes found. Ensure you are logged into creator.xiaohongshu.com and the account has published notes.');
|
|
214
458
|
}
|
|
215
459
|
return notes
|
|
216
460
|
.slice(0, limit)
|
|
@@ -227,3 +471,9 @@ cli({
|
|
|
227
471
|
}));
|
|
228
472
|
},
|
|
229
473
|
});
|
|
474
|
+
export const __test__ = {
|
|
475
|
+
harvestAnalyzeListCaptures,
|
|
476
|
+
isAnalyzeCaptureComplete,
|
|
477
|
+
parseCaptureMapPayload,
|
|
478
|
+
unwrapEvaluateResult,
|
|
479
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
-
import { parseCreatorNoteIdsFromHtml, parseCreatorNotesText } from './creator-notes.js';
|
|
4
|
+
import { __test__, parseCreatorNoteIdsFromHtml, parseCreatorNotesText } from './creator-notes.js';
|
|
4
5
|
import './creator-notes.js';
|
|
5
6
|
function createPageMock(evaluateResult, interceptedRequests = []) {
|
|
6
7
|
const evaluate = Array.isArray(evaluateResult)
|
|
@@ -189,4 +190,92 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
189
190
|
'dddddddddddddddddddddddd',
|
|
190
191
|
]);
|
|
191
192
|
});
|
|
193
|
+
it('harvests captured analyze pages in page order and dedupes note ids', () => {
|
|
194
|
+
const captureMap = {
|
|
195
|
+
'/api/galaxy/creator/datacenter/note/analyze/list?type=0&page_size=10&page_num=2': {
|
|
196
|
+
ok: true,
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
data: {
|
|
199
|
+
total: 3,
|
|
200
|
+
note_infos: [
|
|
201
|
+
{ id: 'bbbbbbbbbbbbbbbbbbbbbbbb', title: 'page 2' },
|
|
202
|
+
{ id: 'aaaaaaaaaaaaaaaaaaaaaaaa', title: 'duplicate from page 2' },
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
207
|
+
'/api/galaxy/creator/datacenter/note/analyze/list?type=0&page_size=10&page_num=1': {
|
|
208
|
+
ok: true,
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
data: {
|
|
211
|
+
total: 3,
|
|
212
|
+
note_infos: [
|
|
213
|
+
{ id: 'aaaaaaaaaaaaaaaaaaaaaaaa', title: 'page 1' },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
expect(__test__.harvestAnalyzeListCaptures(captureMap)).toEqual({
|
|
220
|
+
total: 3,
|
|
221
|
+
items: [
|
|
222
|
+
{ id: 'aaaaaaaaaaaaaaaaaaaaaaaa', title: 'page 1' },
|
|
223
|
+
{ id: 'bbbbbbbbbbbbbbbbbbbbbbbb', title: 'page 2' },
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
it('treats incomplete captured pagination as fallback-needed instead of partial success', () => {
|
|
228
|
+
const firstPageItems = Array.from({ length: 10 }, (_, index) => ({
|
|
229
|
+
id: String(index).padStart(24, '0'),
|
|
230
|
+
}));
|
|
231
|
+
expect(__test__.isAnalyzeCaptureComplete(firstPageItems, 25, 20)).toBe(false);
|
|
232
|
+
expect(__test__.isAnalyzeCaptureComplete(firstPageItems, 25, 10)).toBe(true);
|
|
233
|
+
expect(__test__.isAnalyzeCaptureComplete(firstPageItems, 0, 20)).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
it('unwraps browser bridge capture-map envelopes', () => {
|
|
236
|
+
const captureMap = {
|
|
237
|
+
'/api/galaxy/creator/datacenter/note/analyze/list?page_num=1': {
|
|
238
|
+
ok: true,
|
|
239
|
+
body: '{"data":{"total":0,"note_infos":[]}}',
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
expect(__test__.parseCaptureMapPayload({ session: 'site:xiaohongshu', data: JSON.stringify(captureMap) })).toEqual(captureMap);
|
|
243
|
+
expect(__test__.parseCaptureMapPayload({ session: 'site:xiaohongshu', data: captureMap })).toEqual(captureMap);
|
|
244
|
+
});
|
|
245
|
+
it('does not fall back to partial DOM rows when captured total proves pagination is incomplete', async () => {
|
|
246
|
+
const cmd = getRegistry().get('xiaohongshu/creator-notes');
|
|
247
|
+
const captureMap = {
|
|
248
|
+
'/api/galaxy/creator/datacenter/note/analyze/list?type=0&page_size=10&page_num=1': {
|
|
249
|
+
ok: true,
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
data: {
|
|
252
|
+
total: 25,
|
|
253
|
+
note_infos: Array.from({ length: 10 }, (_, index) => ({
|
|
254
|
+
id: String(index).padStart(24, '0'),
|
|
255
|
+
title: `note ${index}`,
|
|
256
|
+
})),
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
const page = createPageMock(undefined);
|
|
262
|
+
page.evaluate = vi.fn()
|
|
263
|
+
.mockResolvedValueOnce(undefined)
|
|
264
|
+
.mockResolvedValueOnce(undefined)
|
|
265
|
+
.mockResolvedValueOnce(JSON.stringify(captureMap))
|
|
266
|
+
.mockResolvedValueOnce(false);
|
|
267
|
+
|
|
268
|
+
await expect(cmd.func(page, { limit: 20 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
269
|
+
});
|
|
270
|
+
it('throws EmptyResultError when the creator account has no notes', async () => {
|
|
271
|
+
const cmd = getRegistry().get('xiaohongshu/creator-notes');
|
|
272
|
+
const page = createPageMock(undefined);
|
|
273
|
+
page.evaluate = vi.fn()
|
|
274
|
+
.mockResolvedValueOnce(undefined)
|
|
275
|
+
.mockResolvedValueOnce(undefined)
|
|
276
|
+
.mockResolvedValueOnce([])
|
|
277
|
+
.mockResolvedValueOnce({ text: '', html: '' });
|
|
278
|
+
|
|
279
|
+
await expect(cmd.func(page, { limit: 1 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
280
|
+
});
|
|
192
281
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Requires: logged into creator.xiaohongshu.com in Chrome.
|
|
9
9
|
*/
|
|
10
10
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
11
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
11
12
|
cli({
|
|
12
13
|
site: 'xiaohongshu',
|
|
13
14
|
name: 'creator-stats',
|
|
@@ -52,7 +53,7 @@ cli({
|
|
|
52
53
|
}
|
|
53
54
|
const stats = data.data[period];
|
|
54
55
|
if (!stats) {
|
|
55
|
-
throw new
|
|
56
|
+
throw new EmptyResultError('xiaohongshu creator-stats', `No data for period "${period}". Available: ${Object.keys(data.data).join(', ')}`);
|
|
56
57
|
}
|
|
57
58
|
// Format daily trend as sparkline-like summary
|
|
58
59
|
const formatTrend = (list) => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import './creator-stats.js';
|
|
5
|
+
|
|
6
|
+
describe('xiaohongshu creator-stats', () => {
|
|
7
|
+
it('throws EmptyResultError when the requested stats period has no data', async () => {
|
|
8
|
+
const cmd = getRegistry().get('xiaohongshu/creator-stats');
|
|
9
|
+
const page = {
|
|
10
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
evaluate: vi.fn().mockResolvedValue({
|
|
12
|
+
data: {
|
|
13
|
+
seven: null,
|
|
14
|
+
thirty: {
|
|
15
|
+
view_count: 1,
|
|
16
|
+
view_list: [{ count: 1 }],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
await expect(cmd.func(page, { period: 'seven' })).rejects.toBeInstanceOf(EmptyResultError);
|
|
23
|
+
});
|
|
24
|
+
});
|