@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,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaohongshu delete-note: remove a published note via creator center UI.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Navigate to creator note-manager
|
|
6
|
+
* 2. Switch to "已发布" tab (delete is only available on published notes;
|
|
7
|
+
* "审核中" and "未通过" rows do not expose a web delete entry, only mobile)
|
|
8
|
+
* 3. Locate the row whose `data-impression` JSON contains the target noteId
|
|
9
|
+
* 4. Click the inline `<span class="control data-del">` action
|
|
10
|
+
* 5. Click "确定" in the `.d-modal-footer` confirmation modal
|
|
11
|
+
* 6. Poll for the row disappearing from the list
|
|
12
|
+
*
|
|
13
|
+
* Requires: logged into creator.xiaohongshu.com in Chrome.
|
|
14
|
+
*/
|
|
15
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
16
|
+
import { ArgumentError, AuthRequiredError, CliError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
17
|
+
const NOTE_MANAGER_URL = 'https://creator.xiaohongshu.com/new/note-manager';
|
|
18
|
+
const ROW_SETTLE_MS = 3000;
|
|
19
|
+
const MODAL_SETTLE_MS = 2000;
|
|
20
|
+
const VERIFY_TIMEOUT_MS = 10_000;
|
|
21
|
+
const VERIFY_POLL_MS = 1000;
|
|
22
|
+
const NOTE_ID_RE = /^[0-9a-f]{24}$/i;
|
|
23
|
+
function unwrapEvaluateResult(payload) {
|
|
24
|
+
if (payload && typeof payload === 'object' && 'session' in payload && 'data' in payload) {
|
|
25
|
+
return payload.data;
|
|
26
|
+
}
|
|
27
|
+
return payload;
|
|
28
|
+
}
|
|
29
|
+
function requireEvaluateString(payload, context) {
|
|
30
|
+
if (typeof payload !== 'string') {
|
|
31
|
+
throw new CommandExecutionError(`xiaohongshu/delete-note: malformed ${context} payload`);
|
|
32
|
+
}
|
|
33
|
+
return payload;
|
|
34
|
+
}
|
|
35
|
+
function requireEvaluateBoolean(payload, context) {
|
|
36
|
+
if (typeof payload !== 'boolean') {
|
|
37
|
+
throw new CommandExecutionError(`xiaohongshu/delete-note: malformed ${context} payload`);
|
|
38
|
+
}
|
|
39
|
+
return payload;
|
|
40
|
+
}
|
|
41
|
+
function requireEvaluateObject(payload, context) {
|
|
42
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
43
|
+
throw new CommandExecutionError(`xiaohongshu/delete-note: malformed ${context} payload`);
|
|
44
|
+
}
|
|
45
|
+
return payload;
|
|
46
|
+
}
|
|
47
|
+
function requireActionResult(payload, context) {
|
|
48
|
+
const result = requireEvaluateObject(payload, context);
|
|
49
|
+
if (typeof result.ok !== 'boolean') {
|
|
50
|
+
throw new CommandExecutionError(`xiaohongshu/delete-note: malformed ${context} payload`);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
function isXiaohongshuHost(hostname) {
|
|
55
|
+
const host = String(hostname || '').toLowerCase();
|
|
56
|
+
return host === 'xiaohongshu.com' || host.endsWith('.xiaohongshu.com');
|
|
57
|
+
}
|
|
58
|
+
function isSupportedQueryNoteUrl(url) {
|
|
59
|
+
return url.hostname.toLowerCase() === 'creator.xiaohongshu.com'
|
|
60
|
+
&& url.pathname.replace(/\/+$/, '') === '/statistics/note-detail';
|
|
61
|
+
}
|
|
62
|
+
function normalizeNoteId(input) {
|
|
63
|
+
const raw = String(input ?? '').trim();
|
|
64
|
+
if (!raw) {
|
|
65
|
+
throw new ArgumentError('xiaohongshu/delete-note: note-id cannot be empty');
|
|
66
|
+
}
|
|
67
|
+
if (NOTE_ID_RE.test(raw))
|
|
68
|
+
return raw.toLowerCase();
|
|
69
|
+
if (!/^https:\/\//i.test(raw)) {
|
|
70
|
+
throw new ArgumentError('xiaohongshu/delete-note: note-id must be a 24-character Xiaohongshu note ID or an exact Xiaohongshu note URL');
|
|
71
|
+
}
|
|
72
|
+
let url;
|
|
73
|
+
try {
|
|
74
|
+
url = new URL(raw);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new ArgumentError('xiaohongshu/delete-note: invalid note URL');
|
|
78
|
+
}
|
|
79
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.port || !isXiaohongshuHost(url.hostname)) {
|
|
80
|
+
throw new ArgumentError('xiaohongshu/delete-note: note URL must be an exact https://*.xiaohongshu.com URL');
|
|
81
|
+
}
|
|
82
|
+
const queryId = url.searchParams.get('noteId') || url.searchParams.get('note_id');
|
|
83
|
+
if (queryId && NOTE_ID_RE.test(queryId) && isSupportedQueryNoteUrl(url))
|
|
84
|
+
return queryId.toLowerCase();
|
|
85
|
+
const pathMatch = url.pathname.match(/^\/(?:explore|note|search_result|discovery\/item)\/([0-9a-f]{24})\/?$/i)
|
|
86
|
+
|| url.pathname.match(/^\/user\/profile\/[^/?#]+\/([0-9a-f]{24})\/?$/i);
|
|
87
|
+
if (pathMatch)
|
|
88
|
+
return pathMatch[1].toLowerCase();
|
|
89
|
+
throw new ArgumentError('xiaohongshu/delete-note: note URL must contain a 24-character note ID');
|
|
90
|
+
}
|
|
91
|
+
function buildLocateAndMaybeDeleteScript(noteId, shouldClick) {
|
|
92
|
+
return `
|
|
93
|
+
(cfg => {
|
|
94
|
+
const { targetId, shouldClick } = cfg;
|
|
95
|
+
const isVisible = (el) => !!el && el.offsetParent !== null;
|
|
96
|
+
const matchesNoteId = (impressionRaw) => {
|
|
97
|
+
if (!impressionRaw) return false;
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(impressionRaw);
|
|
100
|
+
const id = parsed && parsed.noteTarget && parsed.noteTarget.value && parsed.noteTarget.value.noteId;
|
|
101
|
+
return typeof id === 'string' && id === targetId;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const notes = Array.from(document.querySelectorAll('.note')).filter(isVisible);
|
|
107
|
+
for (const note of notes) {
|
|
108
|
+
if (matchesNoteId(note.getAttribute('data-impression'))) {
|
|
109
|
+
const del = note.querySelector('span.control.data-del');
|
|
110
|
+
if (!del || !isVisible(del)) {
|
|
111
|
+
return { ok: false, kind: 'no_delete_action', visibleRows: notes.length };
|
|
112
|
+
}
|
|
113
|
+
if (!shouldClick) {
|
|
114
|
+
return { ok: true, clicked: false };
|
|
115
|
+
}
|
|
116
|
+
del.click();
|
|
117
|
+
return { ok: true, clicked: true };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { ok: false, kind: 'not_found', visibleRows: notes.length };
|
|
121
|
+
})(${JSON.stringify({ targetId: noteId, shouldClick })})
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
function buildVerifyGoneScript(noteId) {
|
|
125
|
+
return `
|
|
126
|
+
(targetId => {
|
|
127
|
+
const matchesNoteId = (impressionRaw) => {
|
|
128
|
+
if (!impressionRaw) return false;
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(impressionRaw);
|
|
131
|
+
const id = parsed && parsed.noteTarget && parsed.noteTarget.value && parsed.noteTarget.value.noteId;
|
|
132
|
+
return typeof id === 'string' && id === targetId;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const notes = Array.from(document.querySelectorAll('.note'));
|
|
138
|
+
return notes.some((n) => matchesNoteId(n.getAttribute('data-impression')));
|
|
139
|
+
})(${JSON.stringify(noteId)})
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
cli({
|
|
143
|
+
site: 'xiaohongshu',
|
|
144
|
+
name: 'delete-note',
|
|
145
|
+
access: 'write',
|
|
146
|
+
description: '删除小红书已发布笔记 (creator center UI automation)',
|
|
147
|
+
domain: 'creator.xiaohongshu.com',
|
|
148
|
+
strategy: Strategy.COOKIE,
|
|
149
|
+
navigateBefore: false,
|
|
150
|
+
browser: true,
|
|
151
|
+
args: [
|
|
152
|
+
{
|
|
153
|
+
name: 'note-id',
|
|
154
|
+
required: true,
|
|
155
|
+
positional: true,
|
|
156
|
+
help: 'Note ID (e.g. 6a08ba0b000000000702a893 from xiaohongshu creator-notes / URL)',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'execute',
|
|
160
|
+
type: 'boolean',
|
|
161
|
+
default: false,
|
|
162
|
+
help: 'Actually click delete + confirm. Default is dry-run target verification only.',
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
columns: ['status', 'note_id', 'message'],
|
|
166
|
+
func: async (page, kwargs) => {
|
|
167
|
+
try {
|
|
168
|
+
const noteId = normalizeNoteId(kwargs['note-id']);
|
|
169
|
+
const execute = kwargs.execute === true;
|
|
170
|
+
await page.goto(NOTE_MANAGER_URL);
|
|
171
|
+
await page.wait({ time: ROW_SETTLE_MS / 1000 });
|
|
172
|
+
// Detect login redirect (creator.xiaohongshu.com bounces to /login on auth failure)
|
|
173
|
+
const currentUrl = requireEvaluateString(unwrapEvaluateResult(await page.evaluate('() => location.href')), 'current-url');
|
|
174
|
+
if (typeof currentUrl === 'string' && /\/login(?:[/?#]|$)/i.test(new URL(currentUrl).pathname + new URL(currentUrl).search)) {
|
|
175
|
+
throw new AuthRequiredError('creator.xiaohongshu.com');
|
|
176
|
+
}
|
|
177
|
+
// Step 1: ensure 已发布 tab is active (delete only exposed there).
|
|
178
|
+
const tabClicked = requireEvaluateBoolean(unwrapEvaluateResult(await page.evaluate(`
|
|
179
|
+
() => {
|
|
180
|
+
const isVisible = (el) => !!el && el.offsetParent !== null;
|
|
181
|
+
for (const el of document.querySelectorAll('a, button, [role="tab"], div')) {
|
|
182
|
+
const text = (el.innerText || el.textContent || '').trim();
|
|
183
|
+
if (text === '已发布' && isVisible(el)) {
|
|
184
|
+
el.click();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
`)), 'published-tab');
|
|
191
|
+
if (!tabClicked) {
|
|
192
|
+
throw new CommandExecutionError('xiaohongshu/delete-note: 已发布 tab not found on note-manager; xhs creator UI may have changed.');
|
|
193
|
+
}
|
|
194
|
+
await page.wait({ time: ROW_SETTLE_MS / 1000 });
|
|
195
|
+
// Step 2: locate the .note row whose data-impression JSON carries the
|
|
196
|
+
// exact `noteId` field. Dry-run stops here; execute clicks delete.
|
|
197
|
+
// Substring matching on the raw attribute would risk matching unrelated
|
|
198
|
+
// fields whose values happen to share the noteId prefix, so parse the JSON
|
|
199
|
+
// and compare `noteTarget.value.noteId` explicitly.
|
|
200
|
+
const initResult = requireActionResult(unwrapEvaluateResult(await page.evaluate(buildLocateAndMaybeDeleteScript(noteId, execute))), 'locate-note');
|
|
201
|
+
if (!initResult?.ok) {
|
|
202
|
+
if (initResult?.kind === 'not_found') {
|
|
203
|
+
throw new EmptyResultError('xiaohongshu/delete-note', `Note ${noteId} not visible in the 已发布 tab. Verify the note belongs to the logged-in account and has cleared review (审核中 / 未通过 rows have no web delete entry).`);
|
|
204
|
+
}
|
|
205
|
+
if (initResult?.kind === 'no_delete_action') {
|
|
206
|
+
throw new CommandExecutionError(`xiaohongshu/delete-note: note ${noteId} row found but no delete action visible; xhs creator UI may have changed.`);
|
|
207
|
+
}
|
|
208
|
+
throw new CommandExecutionError('xiaohongshu/delete-note: failed to locate note row');
|
|
209
|
+
}
|
|
210
|
+
if (!execute) {
|
|
211
|
+
return [{ status: 'dry-run', note_id: noteId, message: 'Target note row and delete action verified. Re-run with --execute to delete.' }];
|
|
212
|
+
}
|
|
213
|
+
await page.wait({ time: MODAL_SETTLE_MS / 1000 });
|
|
214
|
+
// Step 3: click "确定" in the `.d-modal-footer` confirmation modal.
|
|
215
|
+
const confirmResult = requireActionResult(unwrapEvaluateResult(await page.evaluate(`
|
|
216
|
+
() => {
|
|
217
|
+
const isVisible = (el) => !!el && el.offsetParent !== null;
|
|
218
|
+
const footer = Array.from(document.querySelectorAll('.d-modal-footer')).find(isVisible);
|
|
219
|
+
if (!footer) return { ok: false, kind: 'no_modal' };
|
|
220
|
+
const buttons = Array.from(footer.querySelectorAll('button, [role="button"]')).filter(isVisible);
|
|
221
|
+
const confirmBtn = buttons.find((b) => (b.innerText || b.textContent || '').trim() === '确定');
|
|
222
|
+
if (!confirmBtn) return { ok: false, kind: 'no_confirm', labels: buttons.map(b => (b.innerText || '').trim()) };
|
|
223
|
+
confirmBtn.click();
|
|
224
|
+
return { ok: true };
|
|
225
|
+
}
|
|
226
|
+
`)), 'confirm-modal');
|
|
227
|
+
if (!confirmResult?.ok) {
|
|
228
|
+
throw new CommandExecutionError(`xiaohongshu/delete-note: confirmation modal step failed (${confirmResult?.kind ?? 'unknown'})`);
|
|
229
|
+
}
|
|
230
|
+
// Step 4: poll for row removal (proves the delete actually committed,
|
|
231
|
+
// not just the modal was clicked). Iteration-bounded rather than
|
|
232
|
+
// wall-clock so tests with a mocked `page.wait` exhaust the loop
|
|
233
|
+
// quickly instead of stalling on real time.
|
|
234
|
+
const VERIFY_ITERATIONS = Math.ceil(VERIFY_TIMEOUT_MS / VERIFY_POLL_MS);
|
|
235
|
+
let stillPresent = true;
|
|
236
|
+
for (let i = 0; i < VERIFY_ITERATIONS; i++) {
|
|
237
|
+
await page.wait({ time: VERIFY_POLL_MS / 1000 });
|
|
238
|
+
const probe = requireEvaluateBoolean(unwrapEvaluateResult(await page.evaluate(buildVerifyGoneScript(noteId))), 'verify-gone');
|
|
239
|
+
if (probe === false) {
|
|
240
|
+
stillPresent = false;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (stillPresent) {
|
|
245
|
+
throw new CommandExecutionError(`xiaohongshu/delete-note: note ${noteId} still visible after confirm click; deletion may not have committed.`);
|
|
246
|
+
}
|
|
247
|
+
return [{ status: 'deleted', note_id: noteId, message: 'Delete confirmed and note row disappeared.' }];
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
if (err instanceof CliError)
|
|
251
|
+
throw err;
|
|
252
|
+
throw new CommandExecutionError(`xiaohongshu/delete-note failed: ${err?.message ?? String(err)}`);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
export const __test__ = {
|
|
257
|
+
normalizeNoteId,
|
|
258
|
+
buildLocateAndMaybeDeleteScript,
|
|
259
|
+
buildVerifyGoneScript,
|
|
260
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
5
|
+
|
|
6
|
+
import { __test__ } from './delete-note.js';
|
|
7
|
+
|
|
8
|
+
function makePage(evaluateResults = []) {
|
|
9
|
+
const evaluate = vi.fn();
|
|
10
|
+
for (const r of evaluateResults) evaluate.mockResolvedValueOnce(r);
|
|
11
|
+
evaluate.mockResolvedValue(undefined);
|
|
12
|
+
return {
|
|
13
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
evaluate,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('xiaohongshu delete-note command', () => {
|
|
20
|
+
const getCommand = () => getRegistry().get('xiaohongshu/delete-note');
|
|
21
|
+
const validId = '6a08ba0b000000000702a893';
|
|
22
|
+
|
|
23
|
+
it('returns deleted status when delete + confirm + verify all succeed', async () => {
|
|
24
|
+
const page = makePage([
|
|
25
|
+
'https://creator.xiaohongshu.com/new/note-manager', // currentUrl
|
|
26
|
+
true, // 已发布 tab click
|
|
27
|
+
{ ok: true, clicked: true }, // initResult: row found + delete clicked
|
|
28
|
+
{ ok: true }, // confirmResult
|
|
29
|
+
false, // verify probe: row gone
|
|
30
|
+
]);
|
|
31
|
+
const result = await getCommand().func(page, { 'note-id': validId, execute: true });
|
|
32
|
+
expect(result).toEqual([
|
|
33
|
+
{ status: 'deleted', note_id: validId, message: 'Delete confirmed and note row disappeared.' },
|
|
34
|
+
]);
|
|
35
|
+
expect(page.goto).toHaveBeenCalledWith('https://creator.xiaohongshu.com/new/note-manager');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('dry-runs by default after verifying the exact target and delete affordance', async () => {
|
|
39
|
+
const page = makePage([
|
|
40
|
+
'https://creator.xiaohongshu.com/new/note-manager',
|
|
41
|
+
true,
|
|
42
|
+
{ ok: true, clicked: false },
|
|
43
|
+
]);
|
|
44
|
+
const result = await getCommand().func(page, { 'note-id': validId });
|
|
45
|
+
expect(result).toEqual([
|
|
46
|
+
{ status: 'dry-run', note_id: validId, message: 'Target note row and delete action verified. Re-run with --execute to delete.' },
|
|
47
|
+
]);
|
|
48
|
+
expect(page.evaluate).toHaveBeenCalledTimes(3);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('unwraps browser bridge envelopes at every evaluate boundary', async () => {
|
|
52
|
+
const page = makePage([
|
|
53
|
+
{ session: 's', data: 'https://creator.xiaohongshu.com/new/note-manager' },
|
|
54
|
+
{ session: 's', data: true },
|
|
55
|
+
{ session: 's', data: { ok: true, clicked: true } },
|
|
56
|
+
{ session: 's', data: { ok: true } },
|
|
57
|
+
{ session: 's', data: false },
|
|
58
|
+
]);
|
|
59
|
+
const result = await getCommand().func(page, { 'note-id': validId, execute: true });
|
|
60
|
+
expect(result[0]).toMatchObject({ status: 'deleted', note_id: validId });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('normalizes exact Xiaohongshu note IDs from supported URL forms', () => {
|
|
64
|
+
expect(__test__.normalizeNoteId(validId.toUpperCase())).toBe(validId);
|
|
65
|
+
expect(__test__.normalizeNoteId(`https://www.xiaohongshu.com/explore/${validId}?xsec_token=t`)).toBe(validId);
|
|
66
|
+
expect(__test__.normalizeNoteId(`https://creator.xiaohongshu.com/statistics/note-detail?noteId=${validId}`)).toBe(validId);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('throws ArgumentError for missing or ambiguous note identity before navigation', async () => {
|
|
70
|
+
const page = makePage();
|
|
71
|
+
await expect(getCommand().func(page, { 'note-id': '' })).rejects.toBeInstanceOf(ArgumentError);
|
|
72
|
+
await expect(getCommand().func(page, { 'note-id': ' ' })).rejects.toBeInstanceOf(ArgumentError);
|
|
73
|
+
await expect(getCommand().func(page, { 'note-id': 'x' })).rejects.toBeInstanceOf(ArgumentError);
|
|
74
|
+
await expect(getCommand().func(page, { 'note-id': 'https://evil.com/explore/6a08ba0b000000000702a893' })).rejects.toBeInstanceOf(ArgumentError);
|
|
75
|
+
await expect(getCommand().func(page, { 'note-id': 'https://xhslink.com/abc' })).rejects.toBeInstanceOf(ArgumentError);
|
|
76
|
+
await expect(getCommand().func(page, { 'note-id': `https://www.xiaohongshu.com/anything?noteId=${validId}` })).rejects.toBeInstanceOf(ArgumentError);
|
|
77
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('throws CommandExecutionError for malformed evaluate payloads instead of trusting truthy objects', async () => {
|
|
81
|
+
await expect(getCommand().func(makePage([
|
|
82
|
+
{ session: 's', data: { href: 'https://creator.xiaohongshu.com/new/note-manager' } },
|
|
83
|
+
]), { 'note-id': validId })).rejects.toThrowError(/malformed current-url payload/);
|
|
84
|
+
|
|
85
|
+
await expect(getCommand().func(makePage([
|
|
86
|
+
'https://creator.xiaohongshu.com/new/note-manager',
|
|
87
|
+
{ session: 's', data: { ok: true } },
|
|
88
|
+
]), { 'note-id': validId })).rejects.toThrowError(/malformed published-tab payload/);
|
|
89
|
+
|
|
90
|
+
await expect(getCommand().func(makePage([
|
|
91
|
+
'https://creator.xiaohongshu.com/new/note-manager',
|
|
92
|
+
true,
|
|
93
|
+
{ session: 's', data: { ok: 'yes', clicked: false } },
|
|
94
|
+
]), { 'note-id': validId })).rejects.toThrowError(/malformed locate-note payload/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('throws AuthRequiredError when redirected to login', async () => {
|
|
98
|
+
const page = makePage([
|
|
99
|
+
'https://creator.xiaohongshu.com/login?redirectReason=401',
|
|
100
|
+
]);
|
|
101
|
+
await expect(getCommand().func(page, { 'note-id': validId })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('throws CommandExecutionError when 已发布 tab cannot be clicked (UI drift)', async () => {
|
|
105
|
+
const page = makePage([
|
|
106
|
+
'https://creator.xiaohongshu.com/new/note-manager',
|
|
107
|
+
false, // tab click returns false
|
|
108
|
+
]);
|
|
109
|
+
await expect(getCommand().func(page, { 'note-id': validId })).rejects.toThrowError(/已发布 tab not found/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('throws EmptyResultError when the note row is not in the 已发布 tab', async () => {
|
|
113
|
+
const page = makePage([
|
|
114
|
+
'https://creator.xiaohongshu.com/new/note-manager',
|
|
115
|
+
true,
|
|
116
|
+
{ ok: false, kind: 'not_found', visibleRows: 0 },
|
|
117
|
+
]);
|
|
118
|
+
await expect(getCommand().func(page, { 'note-id': validId })).rejects.toBeInstanceOf(EmptyResultError);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('throws CommandExecutionError when the row has no visible delete action', async () => {
|
|
122
|
+
const page = makePage([
|
|
123
|
+
'https://creator.xiaohongshu.com/new/note-manager',
|
|
124
|
+
true,
|
|
125
|
+
{ ok: false, kind: 'no_delete_action', visibleRows: 1 },
|
|
126
|
+
]);
|
|
127
|
+
await expect(getCommand().func(page, { 'note-id': validId })).rejects.toThrowError(/no delete action/i);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('throws CommandExecutionError when the confirmation modal does not appear', async () => {
|
|
131
|
+
const page = makePage([
|
|
132
|
+
'https://creator.xiaohongshu.com/new/note-manager',
|
|
133
|
+
true,
|
|
134
|
+
{ ok: true },
|
|
135
|
+
{ ok: false, kind: 'no_modal' },
|
|
136
|
+
]);
|
|
137
|
+
await expect(getCommand().func(page, { 'note-id': validId, execute: true })).rejects.toThrowError(/no_modal/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('throws CommandExecutionError when row stays visible after confirm (delete did not commit)', async () => {
|
|
141
|
+
// verify probes return true (note still present) for the entire poll window.
|
|
142
|
+
const probes = Array(15).fill(true);
|
|
143
|
+
const page = makePage([
|
|
144
|
+
'https://creator.xiaohongshu.com/new/note-manager',
|
|
145
|
+
true,
|
|
146
|
+
{ ok: true },
|
|
147
|
+
{ ok: true },
|
|
148
|
+
...probes,
|
|
149
|
+
]);
|
|
150
|
+
await expect(getCommand().func(page, { 'note-id': validId, execute: true })).rejects.toThrowError(/still visible after confirm/i);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('executes the generated row locator without substring-matching other impression fields', () => {
|
|
154
|
+
const otherId = '6a08ba0b000000000702a894';
|
|
155
|
+
const dom = new JSDOM(`
|
|
156
|
+
<div class="note" data-impression='{"noteTarget":{"value":{"noteId":"${otherId}"}},"title":"${validId}"}'>
|
|
157
|
+
<span class="control data-del">删除</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="note" data-impression='{"noteTarget":{"value":{"noteId":"${validId}"}}}'>
|
|
160
|
+
<span class="control data-del">删除</span>
|
|
161
|
+
</div>
|
|
162
|
+
`, { runScripts: 'outside-only' });
|
|
163
|
+
Object.defineProperty(dom.window.HTMLElement.prototype, 'offsetParent', {
|
|
164
|
+
configurable: true,
|
|
165
|
+
get() {
|
|
166
|
+
return this.ownerDocument.body;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const result = dom.window.eval(__test__.buildLocateAndMaybeDeleteScript(validId, false));
|
|
170
|
+
expect(result).toEqual({ ok: true, clicked: false });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -52,49 +52,108 @@ export function buildDownloadExtractJs(noteId) {
|
|
|
52
52
|
const authorEl = document.querySelector('.username, .author-name, .name');
|
|
53
53
|
result.author = authorEl?.textContent?.trim() || 'unknown';
|
|
54
54
|
|
|
55
|
-
// Get images
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
'.note-image img',
|
|
61
|
-
'.image-wrapper img',
|
|
62
|
-
'#noteContainer .media-container img[src*="xhscdn"]',
|
|
63
|
-
'img[src*="ci.xiaohongshu.com"]'
|
|
64
|
-
];
|
|
55
|
+
// Get images: prefer canonical carousel order from __INITIAL_STATE__
|
|
56
|
+
// so the saved order matches what the user sees on the platform (#1514).
|
|
57
|
+
// DOM extraction is used only as a fallback because multiple selectors,
|
|
58
|
+
// hidden / duplicated / preloaded slides, and lazy rendering can reorder
|
|
59
|
+
// the discovered nodes away from the platform's display order.
|
|
65
60
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
const normalizeImageUrl = (raw) => {
|
|
62
|
+
if (!raw || typeof raw !== 'string') return '';
|
|
63
|
+
let src = raw.split('?')[0];
|
|
64
|
+
src = src.replace(/\\/imageView\\d+\\/\\d+\\/w\\/\\d+/, '');
|
|
65
|
+
return src;
|
|
66
|
+
};
|
|
67
|
+
const orderedImageUrls = [];
|
|
68
|
+
const seenImageUrls = new Set();
|
|
69
|
+
const pushImage = (url) => {
|
|
70
|
+
if (!url || seenImageUrls.has(url)) return;
|
|
71
|
+
seenImageUrls.add(url);
|
|
72
|
+
orderedImageUrls.push(url);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getStructuredNotes = () => {
|
|
76
|
+
const state = window.__INITIAL_STATE__;
|
|
77
|
+
const noteData = state?.note?.noteDetailMap || state?.note?.note || {};
|
|
78
|
+
if (!noteData || typeof noteData !== 'object') return [];
|
|
79
|
+
const currentIds = [...new Set([result.noteId, '${noteId}'].filter(Boolean))];
|
|
80
|
+
const notes = [];
|
|
81
|
+
for (const id of currentIds) {
|
|
82
|
+
const entry = noteData[id];
|
|
83
|
+
const note = entry?.note || entry;
|
|
84
|
+
if (note && typeof note === 'object') notes.push(note);
|
|
85
|
+
}
|
|
86
|
+
// Compatibility fallback for legacy single-note stores. Do not use this
|
|
87
|
+
// when keyed detail maps contain multiple notes, or carousel order can
|
|
88
|
+
// be polluted by preloaded/previous note entries.
|
|
89
|
+
const keys = Object.keys(noteData);
|
|
90
|
+
if (notes.length === 0 && keys.length === 1) {
|
|
91
|
+
const entry = noteData[keys[0]];
|
|
92
|
+
const note = entry?.note || entry;
|
|
93
|
+
if (note && typeof note === 'object') notes.push(note);
|
|
94
|
+
}
|
|
95
|
+
return notes;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Method 1: walk __INITIAL_STATE__.note.noteDetailMap[id].note.imageList
|
|
99
|
+
// in array order. Each entry exposes urlDefault as the canonical CDN URL.
|
|
100
|
+
let imageInitialStateUsed = false;
|
|
101
|
+
try {
|
|
102
|
+
for (const note of getStructuredNotes()) {
|
|
103
|
+
const list = Array.isArray(note?.imageList) ? note.imageList : [];
|
|
104
|
+
for (const item of list) {
|
|
105
|
+
const candidate = item?.urlDefault || item?.urlPre || item?.url
|
|
106
|
+
|| item?.infoList?.find(i => i?.imageScene === 'WB_DFT')?.url
|
|
107
|
+
|| item?.infoList?.[0]?.url
|
|
108
|
+
|| '';
|
|
109
|
+
const src = normalizeImageUrl(candidate);
|
|
110
|
+
if (src && (src.includes('xhscdn') || src.includes('xiaohongshu') || src.includes('rednote'))) {
|
|
111
|
+
pushImage(src);
|
|
112
|
+
imageInitialStateUsed = true;
|
|
113
|
+
}
|
|
74
114
|
}
|
|
75
|
-
}
|
|
115
|
+
}
|
|
116
|
+
} catch(e) {}
|
|
117
|
+
|
|
118
|
+
// Method 2: fallback to DOM scraping when the structured state is missing
|
|
119
|
+
// (e.g. preview pages without full SSR hydration). Order may differ from
|
|
120
|
+
// the carousel; surface it anyway rather than returning zero images.
|
|
121
|
+
if (!imageInitialStateUsed) {
|
|
122
|
+
const imageSelectors = [
|
|
123
|
+
'.swiper-slide img',
|
|
124
|
+
'.carousel-image img',
|
|
125
|
+
'.note-slider img',
|
|
126
|
+
'.note-image img',
|
|
127
|
+
'.image-wrapper img',
|
|
128
|
+
'#noteContainer .media-container img[src*="xhscdn"]',
|
|
129
|
+
'img[src*="ci.xiaohongshu.com"]'
|
|
130
|
+
];
|
|
131
|
+
for (const selector of imageSelectors) {
|
|
132
|
+
document.querySelectorAll(selector).forEach(img => {
|
|
133
|
+
const raw = img.src || img.getAttribute('data-src') || '';
|
|
134
|
+
const src = normalizeImageUrl(raw);
|
|
135
|
+
if (src && (src.includes('xhscdn') || src.includes('xiaohongshu') || src.includes('rednote'))) {
|
|
136
|
+
pushImage(src);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
76
140
|
}
|
|
77
141
|
|
|
78
142
|
// Get video — prefer real URL from page state over blob: URLs
|
|
79
143
|
|
|
80
144
|
// Method 1: Extract from __INITIAL_STATE__ (SSR hydration data)
|
|
81
145
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
const streams = video.media?.stream?.h264 || [];
|
|
95
|
-
for (const stream of streams) {
|
|
96
|
-
if (stream.masterUrl) pushMedia('video', stream.masterUrl);
|
|
97
|
-
}
|
|
146
|
+
for (const note of getStructuredNotes()) {
|
|
147
|
+
const video = note?.video;
|
|
148
|
+
if (video) {
|
|
149
|
+
const vUrl = video.url || video.originVideoKey || video.consumer?.originVideoKey;
|
|
150
|
+
if (vUrl) {
|
|
151
|
+
const fullUrl = vUrl.startsWith('http') ? vUrl : 'https://sns-video-bd.xhscdn.com/' + vUrl;
|
|
152
|
+
pushMedia('video', fullUrl);
|
|
153
|
+
}
|
|
154
|
+
const streams = video.media?.stream?.h264 || [];
|
|
155
|
+
for (const stream of streams) {
|
|
156
|
+
if (stream.masterUrl) pushMedia('video', stream.masterUrl);
|
|
98
157
|
}
|
|
99
158
|
}
|
|
100
159
|
}
|
|
@@ -135,10 +194,9 @@ export function buildDownloadExtractJs(noteId) {
|
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
196
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
197
|
+
// Preserve the pre-existing media type order (videos first, then images)
|
|
198
|
+
// while keeping image carousel order stable within the image batch.
|
|
199
|
+
orderedImageUrls.forEach(url => pushMedia('image', url));
|
|
142
200
|
|
|
143
201
|
return result;
|
|
144
202
|
})()
|