@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
|
@@ -10,7 +10,9 @@ vi.mock('@jackwener/opencli/download', () => ({
|
|
|
10
10
|
formatCookieHeader: mockFormatCookieHeader,
|
|
11
11
|
}));
|
|
12
12
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
13
|
+
import { JSDOM } from 'jsdom';
|
|
13
14
|
import './download.js';
|
|
15
|
+
import { buildDownloadExtractJs } from './download.js';
|
|
14
16
|
function createPageMock(evaluateResult) {
|
|
15
17
|
return {
|
|
16
18
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
@@ -113,3 +115,202 @@ describe('xiaohongshu download', () => {
|
|
|
113
115
|
expect(mockDownloadMedia).not.toHaveBeenCalled();
|
|
114
116
|
});
|
|
115
117
|
});
|
|
118
|
+
|
|
119
|
+
describe('xiaohongshu download buildDownloadExtractJs carousel ordering (JSDOM)', () => {
|
|
120
|
+
function runExtract({ html = '', initialState = null, url = 'https://www.xiaohongshu.com/explore/69f9716c000000003601f90e' } = {}) {
|
|
121
|
+
const dom = new JSDOM(`<!doctype html><html><body>${html}</body></html>`, { url, runScripts: 'outside-only' });
|
|
122
|
+
if (initialState) {
|
|
123
|
+
dom.window.__INITIAL_STATE__ = initialState;
|
|
124
|
+
}
|
|
125
|
+
const js = buildDownloadExtractJs('69f9716c000000003601f90e');
|
|
126
|
+
return dom.window.eval(js);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it('preserves carousel order from __INITIAL_STATE__ imageList over DOM discovery order', () => {
|
|
130
|
+
const initialState = {
|
|
131
|
+
note: {
|
|
132
|
+
noteDetailMap: {
|
|
133
|
+
'69f9716c000000003601f90e': {
|
|
134
|
+
note: {
|
|
135
|
+
imageList: [
|
|
136
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/canonical-cover.jpg' },
|
|
137
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/canonical-second.jpg' },
|
|
138
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/canonical-third.jpg' },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
// DOM has the same images but in a DIFFERENT order: repro for #1514.
|
|
146
|
+
const html = `
|
|
147
|
+
<div class="swiper-slide"><img src="https://sns-img-bd.xhscdn.com/canonical-second.jpg" /></div>
|
|
148
|
+
<div class="swiper-slide"><img src="https://sns-img-bd.xhscdn.com/canonical-cover.jpg" /></div>
|
|
149
|
+
<div class="swiper-slide"><img src="https://sns-img-bd.xhscdn.com/canonical-third.jpg" /></div>
|
|
150
|
+
`;
|
|
151
|
+
const result = runExtract({ html, initialState });
|
|
152
|
+
const images = result.media.filter((m) => m.type === 'image');
|
|
153
|
+
expect(images.map((m) => m.url)).toEqual([
|
|
154
|
+
'https://sns-img-bd.xhscdn.com/canonical-cover.jpg',
|
|
155
|
+
'https://sns-img-bd.xhscdn.com/canonical-second.jpg',
|
|
156
|
+
'https://sns-img-bd.xhscdn.com/canonical-third.jpg',
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('prefers urlDefault but falls back to urlPre / url / infoList.WB_DFT / infoList[0]', () => {
|
|
161
|
+
const initialState = {
|
|
162
|
+
note: {
|
|
163
|
+
noteDetailMap: {
|
|
164
|
+
'69f9716c000000003601f90e': {
|
|
165
|
+
note: {
|
|
166
|
+
imageList: [
|
|
167
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/a.jpg' },
|
|
168
|
+
{ urlPre: 'https://sns-img-bd.xhscdn.com/b.jpg' },
|
|
169
|
+
{ url: 'https://sns-img-bd.xhscdn.com/c.jpg' },
|
|
170
|
+
{ infoList: [{ imageScene: 'WB_PRV', url: 'https://sns-img-bd.xhscdn.com/d-low.jpg' }, { imageScene: 'WB_DFT', url: 'https://sns-img-bd.xhscdn.com/d.jpg' }] },
|
|
171
|
+
{ infoList: [{ url: 'https://sns-img-bd.xhscdn.com/e.jpg' }] },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const result = runExtract({ initialState });
|
|
179
|
+
const urls = result.media.filter((m) => m.type === 'image').map((m) => m.url);
|
|
180
|
+
expect(urls).toEqual([
|
|
181
|
+
'https://sns-img-bd.xhscdn.com/a.jpg',
|
|
182
|
+
'https://sns-img-bd.xhscdn.com/b.jpg',
|
|
183
|
+
'https://sns-img-bd.xhscdn.com/c.jpg',
|
|
184
|
+
'https://sns-img-bd.xhscdn.com/d.jpg',
|
|
185
|
+
'https://sns-img-bd.xhscdn.com/e.jpg',
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('strips imageView resize params + query strings from canonical urls', () => {
|
|
190
|
+
const initialState = {
|
|
191
|
+
note: {
|
|
192
|
+
noteDetailMap: {
|
|
193
|
+
'69f9716c000000003601f90e': {
|
|
194
|
+
note: {
|
|
195
|
+
imageList: [
|
|
196
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/raw/imageView2/2/w/1080/cover.jpg?expires=123' },
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
const result = runExtract({ initialState });
|
|
204
|
+
const urls = result.media.filter((m) => m.type === 'image').map((m) => m.url);
|
|
205
|
+
expect(urls).toEqual(['https://sns-img-bd.xhscdn.com/raw/cover.jpg']);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('falls back to DOM extraction when __INITIAL_STATE__ omits imageList', () => {
|
|
209
|
+
const html = `
|
|
210
|
+
<div class="swiper-slide"><img src="https://sns-img-bd.xhscdn.com/dom-1.jpg" /></div>
|
|
211
|
+
<div class="swiper-slide"><img src="https://sns-img-bd.xhscdn.com/dom-2.jpg" /></div>
|
|
212
|
+
`;
|
|
213
|
+
const result = runExtract({ html });
|
|
214
|
+
const urls = result.media.filter((m) => m.type === 'image').map((m) => m.url);
|
|
215
|
+
expect(urls).toEqual([
|
|
216
|
+
'https://sns-img-bd.xhscdn.com/dom-1.jpg',
|
|
217
|
+
'https://sns-img-bd.xhscdn.com/dom-2.jpg',
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('skips non-xhscdn / non-xiaohongshu / non-rednote urls in initial state', () => {
|
|
222
|
+
const initialState = {
|
|
223
|
+
note: {
|
|
224
|
+
noteDetailMap: {
|
|
225
|
+
'69f9716c000000003601f90e': {
|
|
226
|
+
note: {
|
|
227
|
+
imageList: [
|
|
228
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/keep.jpg' },
|
|
229
|
+
{ urlDefault: 'https://imgur.com/drop.jpg' },
|
|
230
|
+
{ urlDefault: '' },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const result = runExtract({ initialState });
|
|
238
|
+
const urls = result.media.filter((m) => m.type === 'image').map((m) => m.url);
|
|
239
|
+
expect(urls).toEqual(['https://sns-img-bd.xhscdn.com/keep.jpg']);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('does not run DOM fallback when initial state yielded any image (preserves canonical order)', () => {
|
|
243
|
+
const initialState = {
|
|
244
|
+
note: {
|
|
245
|
+
noteDetailMap: {
|
|
246
|
+
'69f9716c000000003601f90e': {
|
|
247
|
+
note: {
|
|
248
|
+
imageList: [
|
|
249
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/canonical-only.jpg' },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
const html = `
|
|
257
|
+
<div class="swiper-slide"><img src="https://sns-img-bd.xhscdn.com/dom-extra.jpg" /></div>
|
|
258
|
+
`;
|
|
259
|
+
const result = runExtract({ html, initialState });
|
|
260
|
+
const urls = result.media.filter((m) => m.type === 'image').map((m) => m.url);
|
|
261
|
+
expect(urls).toEqual(['https://sns-img-bd.xhscdn.com/canonical-only.jpg']);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('uses only the current note entry from multi-note initial state maps', () => {
|
|
265
|
+
const initialState = {
|
|
266
|
+
note: {
|
|
267
|
+
noteDetailMap: {
|
|
268
|
+
othernote0000000000000001: {
|
|
269
|
+
note: {
|
|
270
|
+
imageList: [{ urlDefault: 'https://sns-img-bd.xhscdn.com/other.jpg' }],
|
|
271
|
+
video: { url: 'https://sns-video-bd.xhscdn.com/other.mp4' },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
'69f9716c000000003601f90e': {
|
|
275
|
+
note: {
|
|
276
|
+
imageList: [
|
|
277
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/current-1.jpg' },
|
|
278
|
+
{ urlDefault: 'https://sns-img-bd.xhscdn.com/current-2.jpg' },
|
|
279
|
+
],
|
|
280
|
+
video: { url: 'https://sns-video-bd.xhscdn.com/current.mp4' },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
const result = runExtract({ initialState });
|
|
287
|
+
const images = result.media.filter((m) => m.type === 'image').map((m) => m.url);
|
|
288
|
+
const videos = result.media.filter((m) => m.type === 'video').map((m) => m.url);
|
|
289
|
+
expect(images).toEqual([
|
|
290
|
+
'https://sns-img-bd.xhscdn.com/current-1.jpg',
|
|
291
|
+
'https://sns-img-bd.xhscdn.com/current-2.jpg',
|
|
292
|
+
]);
|
|
293
|
+
expect(videos).toEqual(['https://sns-video-bd.xhscdn.com/current.mp4']);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('still resolves videos from __INITIAL_STATE__ alongside the image fix', () => {
|
|
297
|
+
const initialState = {
|
|
298
|
+
note: {
|
|
299
|
+
noteDetailMap: {
|
|
300
|
+
'69f9716c000000003601f90e': {
|
|
301
|
+
note: {
|
|
302
|
+
imageList: [{ urlDefault: 'https://sns-img-bd.xhscdn.com/cover.jpg' }],
|
|
303
|
+
video: { url: 'https://sns-video-bd.xhscdn.com/test.mp4' },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
const result = runExtract({ initialState });
|
|
310
|
+
const images = result.media.filter((m) => m.type === 'image').map((m) => m.url);
|
|
311
|
+
const videos = result.media.filter((m) => m.type === 'video').map((m) => m.url);
|
|
312
|
+
expect(images).toEqual(['https://sns-img-bd.xhscdn.com/cover.jpg']);
|
|
313
|
+
expect(videos).toEqual(['https://sns-video-bd.xhscdn.com/test.mp4']);
|
|
314
|
+
expect(result.media.map((m) => m.type)).toEqual(['video', 'image']);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -22,6 +22,14 @@ const PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?from=menu_l
|
|
|
22
22
|
const MAX_IMAGES = 9;
|
|
23
23
|
const MAX_TITLE_LEN = 20;
|
|
24
24
|
const UPLOAD_SETTLE_MS = 3000;
|
|
25
|
+
/**
|
|
26
|
+
* XHS creator center wraps the publish/save button in an `<xhs-publish-btn>`
|
|
27
|
+
* web component backed by a CLOSED shadow root. Host-level `.click()` does
|
|
28
|
+
* not dispatch into the internal handler. Invoke these instance methods on
|
|
29
|
+
* the host element to trigger publish / save-draft directly (#1606).
|
|
30
|
+
*/
|
|
31
|
+
const PUBLISH_METHOD_NAMES = ['_onPublish', 'onPublish', '_onSubmit', '_handlePublish'];
|
|
32
|
+
const DRAFT_METHOD_NAMES = ['_onSave', '_onSaveDraft', '_onDraft'];
|
|
25
33
|
/** Selectors for the title field, ordered by priority across current UI variants. */
|
|
26
34
|
const TITLE_SELECTORS = [
|
|
27
35
|
// Some creator-center variants expose the title as contenteditable,
|
|
@@ -610,26 +618,58 @@ cli({
|
|
|
610
618
|
}
|
|
611
619
|
// ── Step 7: Publish or save draft ─────────────────────────────────────────
|
|
612
620
|
const actionLabels = isDraft ? ['暂存离开', '存草稿'] : ['发布', '发布笔记'];
|
|
613
|
-
const
|
|
614
|
-
(
|
|
621
|
+
const invokeResult = await page.evaluate(`
|
|
622
|
+
(cfg => {
|
|
623
|
+
const { isDraftMode, publishNames, draftNames, labels } = cfg;
|
|
624
|
+
const isVisible = (el) => {
|
|
625
|
+
if (!el || el.offsetParent === null) return false;
|
|
626
|
+
const rect = el.getBoundingClientRect();
|
|
627
|
+
return rect.width > 0 && rect.height > 0;
|
|
628
|
+
};
|
|
629
|
+
// Path 1: web component method invoke on <xhs-publish-btn>.
|
|
630
|
+
const hosts = Array.from(document.querySelectorAll('xhs-publish-btn')).filter(isVisible);
|
|
631
|
+
const wanted = isDraftMode ? draftNames : publishNames;
|
|
632
|
+
// Try every host + every candidate; do NOT bail on the first throw
|
|
633
|
+
// (multiple hosts can exist, and a later name may succeed).
|
|
634
|
+
let lastMethodError = null;
|
|
635
|
+
for (const host of hosts) {
|
|
636
|
+
for (const name of wanted) {
|
|
637
|
+
if (typeof host[name] !== 'function') continue;
|
|
638
|
+
try {
|
|
639
|
+
host[name]();
|
|
640
|
+
return { ok: true, via: 'method', name };
|
|
641
|
+
} catch (err) {
|
|
642
|
+
lastMethodError = String(err && err.message || err);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Path 2: legacy <button>/[role=button] text-match click fallback.
|
|
615
647
|
const buttons = document.querySelectorAll('button, [role="button"]');
|
|
616
648
|
for (const btn of buttons) {
|
|
617
649
|
const text = (btn.innerText || btn.textContent || '').trim();
|
|
618
650
|
if (
|
|
619
651
|
labels.some(l => text === l || text.includes(l)) &&
|
|
620
|
-
btn
|
|
652
|
+
isVisible(btn) &&
|
|
621
653
|
!btn.disabled
|
|
622
654
|
) {
|
|
623
655
|
btn.click();
|
|
624
|
-
return true;
|
|
656
|
+
return { ok: true, via: 'click', text };
|
|
625
657
|
}
|
|
626
658
|
}
|
|
627
|
-
return false;
|
|
628
|
-
})(${JSON.stringify(
|
|
659
|
+
return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
|
|
660
|
+
})(${JSON.stringify({
|
|
661
|
+
isDraftMode: isDraft,
|
|
662
|
+
publishNames: PUBLISH_METHOD_NAMES,
|
|
663
|
+
draftNames: DRAFT_METHOD_NAMES,
|
|
664
|
+
labels: actionLabels,
|
|
665
|
+
})})
|
|
629
666
|
`);
|
|
630
|
-
if (!
|
|
667
|
+
if (!invokeResult?.ok) {
|
|
631
668
|
await page.screenshot({ path: '/tmp/xhs_publish_submit_debug.png' });
|
|
632
|
-
|
|
669
|
+
const viaClause = invokeResult?.via ? ` (via=${invokeResult.via})` : '';
|
|
670
|
+
const errorClause = invokeResult?.error ? `, error=${invokeResult.error}` : '';
|
|
671
|
+
const lastMethodClause = invokeResult?.lastMethodError ? `, lastMethodError=${invokeResult.lastMethodError}` : '';
|
|
672
|
+
throw new Error(`Could not trigger "${actionLabels[0]}" action${viaClause}${errorClause}${lastMethodClause}. ` +
|
|
633
673
|
'Debug screenshot: /tmp/xhs_publish_submit_debug.png');
|
|
634
674
|
}
|
|
635
675
|
// ── Step 8: Verify success ─────────────────────────────────────────────────
|
|
@@ -76,8 +76,10 @@ describe('xiaohongshu publish', () => {
|
|
|
76
76
|
? { ok: true, sel: '[contenteditable="true"][placeholder*="标题"]', kind: 'contenteditable', actual: '标题走原生输入' }
|
|
77
77
|
: { ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable', actual: '正文也走原生输入' };
|
|
78
78
|
}
|
|
79
|
+
if (code.includes('xhs-publish-btn'))
|
|
80
|
+
return { ok: true, via: 'click', text: '发布' };
|
|
79
81
|
if (code.includes('labels.some'))
|
|
80
|
-
return
|
|
82
|
+
return false;
|
|
81
83
|
if (code.includes('for (const el of document.querySelectorAll'))
|
|
82
84
|
return '发布成功';
|
|
83
85
|
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
@@ -128,8 +130,10 @@ describe('xiaohongshu publish', () => {
|
|
|
128
130
|
return { ok: false, actual: '' };
|
|
129
131
|
if (code.includes('(function(selectors, text)'))
|
|
130
132
|
return { ok: true, sel: '[contenteditable="true"][placeholder*="标题"]', kind: 'contenteditable', actual: '' };
|
|
133
|
+
if (code.includes('xhs-publish-btn'))
|
|
134
|
+
return { ok: true, via: 'click', text: '发布' };
|
|
131
135
|
if (code.includes('labels.some'))
|
|
132
|
-
return
|
|
136
|
+
return false;
|
|
133
137
|
if (code.includes('for (const el of document.querySelectorAll'))
|
|
134
138
|
return '发布成功';
|
|
135
139
|
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
@@ -177,8 +181,10 @@ describe('xiaohongshu publish', () => {
|
|
|
177
181
|
? { ok: true, actual: '原生失败后回退' }
|
|
178
182
|
: { ok: true, actual: '正文也回退' };
|
|
179
183
|
}
|
|
184
|
+
if (code.includes('xhs-publish-btn'))
|
|
185
|
+
return { ok: true, via: 'click', text: '发布' };
|
|
180
186
|
if (code.includes('labels.some'))
|
|
181
|
-
return
|
|
187
|
+
return false;
|
|
182
188
|
if (code.includes('for (const el of document.querySelectorAll'))
|
|
183
189
|
return '发布成功';
|
|
184
190
|
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
@@ -227,8 +233,10 @@ describe('xiaohongshu publish', () => {
|
|
|
227
233
|
return code.includes('input[maxlength')
|
|
228
234
|
? { ok: false, actual: '' }
|
|
229
235
|
: { ok: true, actual: '正文' };
|
|
236
|
+
if (code.includes('xhs-publish-btn'))
|
|
237
|
+
return { ok: true, via: 'click', text: '发布' };
|
|
230
238
|
if (code.includes('labels.some'))
|
|
231
|
-
return
|
|
239
|
+
return false;
|
|
232
240
|
if (code.includes('for (const el of document.querySelectorAll'))
|
|
233
241
|
return '发布成功';
|
|
234
242
|
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
@@ -259,7 +267,7 @@ describe('xiaohongshu publish', () => {
|
|
|
259
267
|
{ ok: true, actual: 'CDP上传优先' },
|
|
260
268
|
{ ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' },
|
|
261
269
|
{ ok: true, actual: '优先走 setFileInput 主路径' },
|
|
262
|
-
true,
|
|
270
|
+
{ ok: true, via: 'click', text: '发布' },
|
|
263
271
|
'https://creator.xiaohongshu.com/publish/success',
|
|
264
272
|
'发布成功',
|
|
265
273
|
], {
|
|
@@ -301,7 +309,7 @@ describe('xiaohongshu publish', () => {
|
|
|
301
309
|
{ ok: true, actual: 'CDP被拒后回退' },
|
|
302
310
|
{ ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' },
|
|
303
311
|
{ ok: true, actual: 'DataTransfer fallback path' },
|
|
304
|
-
true,
|
|
312
|
+
{ ok: true, via: 'click', text: '发布' },
|
|
305
313
|
'https://creator.xiaohongshu.com/publish/success',
|
|
306
314
|
'发布成功',
|
|
307
315
|
], {
|
|
@@ -366,7 +374,7 @@ describe('xiaohongshu publish', () => {
|
|
|
366
374
|
{ ok: true, actual: 'DeepSeek别乱问' },
|
|
367
375
|
{ ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' },
|
|
368
376
|
{ ok: true, actual: '一篇真实一点的小红书正文' },
|
|
369
|
-
true,
|
|
377
|
+
{ ok: true, via: 'click', text: '发布' },
|
|
370
378
|
'https://creator.xiaohongshu.com/publish/success',
|
|
371
379
|
'发布成功',
|
|
372
380
|
]);
|
|
@@ -390,6 +398,49 @@ describe('xiaohongshu publish', () => {
|
|
|
390
398
|
},
|
|
391
399
|
]);
|
|
392
400
|
});
|
|
401
|
+
it('uses the shadow-DOM method-invoke path when xhs-publish-btn handler succeeds', async () => {
|
|
402
|
+
// Mirrors the previous "selects the image-text tab and publishes successfully"
|
|
403
|
+
// mock sequence but returns `via: 'method', name: '_onPublish'` for the publish
|
|
404
|
+
// trigger evaluate, exercising the shadow-DOM web-component handler path
|
|
405
|
+
// (the primary #1606 fix). Without this case the fix's main path is uncovered.
|
|
406
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
407
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
408
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
409
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
410
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
411
|
+
const page = createPageMock([
|
|
412
|
+
'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
|
|
413
|
+
{ ok: true, target: '上传图文', text: '上传图文' },
|
|
414
|
+
{ state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
|
|
415
|
+
{ ok: true, count: 1 },
|
|
416
|
+
false,
|
|
417
|
+
true, // waitForEditForm: editor appeared
|
|
418
|
+
{ ok: true, sel: 'input[maxlength="20"]', kind: 'input' },
|
|
419
|
+
{ ok: true, actual: 'shadow-dom-test' },
|
|
420
|
+
{ ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' },
|
|
421
|
+
{ ok: true, actual: '走 method-invoke 路径' },
|
|
422
|
+
{ ok: true, via: 'method', name: '_onPublish' }, // shadow-DOM handler success
|
|
423
|
+
'https://creator.xiaohongshu.com/publish/success',
|
|
424
|
+
'发布成功',
|
|
425
|
+
]);
|
|
426
|
+
const result = await cmd.func(page, {
|
|
427
|
+
title: 'shadow-dom-test',
|
|
428
|
+
content: '走 method-invoke 路径',
|
|
429
|
+
images: imagePath,
|
|
430
|
+
topics: '',
|
|
431
|
+
draft: false,
|
|
432
|
+
});
|
|
433
|
+
expect(result).toEqual([
|
|
434
|
+
{
|
|
435
|
+
status: '✅ 发布成功',
|
|
436
|
+
detail: '"shadow-dom-test" · 1张图片 · 发布成功',
|
|
437
|
+
},
|
|
438
|
+
]);
|
|
439
|
+
// The publish-trigger evaluate must have been the shadow-DOM probe (contains
|
|
440
|
+
// 'xhs-publish-btn'), not the legacy `button.click()` fallback alone.
|
|
441
|
+
const evaluateCalls = page.evaluate.mock.calls.map((args) => String(args[0]));
|
|
442
|
+
expect(evaluateCalls.some((code) => code.includes('xhs-publish-btn'))).toBe(true);
|
|
443
|
+
});
|
|
393
444
|
it('fails early with a clear error when still on the video page', async () => {
|
|
394
445
|
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
395
446
|
expect(cmd?.func).toBeTypeOf('function');
|
|
@@ -431,7 +482,7 @@ describe('xiaohongshu publish', () => {
|
|
|
431
482
|
{ ok: true, actual: '延迟切换也能过' },
|
|
432
483
|
{ ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' },
|
|
433
484
|
{ ok: true, actual: '图文页切换慢一点也继续等' },
|
|
434
|
-
true,
|
|
485
|
+
{ ok: true, via: 'click', text: '发布' },
|
|
435
486
|
'https://creator.xiaohongshu.com/publish/success',
|
|
436
487
|
'发布成功',
|
|
437
488
|
]);
|
|
@@ -479,8 +530,10 @@ describe('xiaohongshu publish', () => {
|
|
|
479
530
|
? { ok: true, actual: '停留在发布页也算成功' }
|
|
480
531
|
: { ok: true, actual: '草稿成功提示' };
|
|
481
532
|
}
|
|
533
|
+
if (code.includes('xhs-publish-btn'))
|
|
534
|
+
return { ok: true, via: 'click', text: '发布' };
|
|
482
535
|
if (code.includes('labels.some'))
|
|
483
|
-
return
|
|
536
|
+
return false;
|
|
484
537
|
if (code.includes('for (const el of document.querySelectorAll')) {
|
|
485
538
|
return code.includes('保存成功') ? '保存成功' : '';
|
|
486
539
|
}
|
|
@@ -529,8 +582,10 @@ describe('xiaohongshu publish', () => {
|
|
|
529
582
|
? { ok: true, actual: '发布提示不该复用草稿成功' }
|
|
530
583
|
: { ok: true, actual: '发布成功提示' };
|
|
531
584
|
}
|
|
585
|
+
if (code.includes('xhs-publish-btn'))
|
|
586
|
+
return { ok: true, via: 'click', text: '发布' };
|
|
532
587
|
if (code.includes('labels.some'))
|
|
533
|
-
return
|
|
588
|
+
return false;
|
|
534
589
|
if (code.includes('for (const el of document.querySelectorAll')) {
|
|
535
590
|
return code.includes('保存成功') ? '保存成功' : '';
|
|
536
591
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { assertReadableUserSnapshot } from './user.js';
|
|
2
4
|
import { buildXhsNoteUrl, extractXhsUserNotes, flattenXhsNoteGroups, normalizeXhsUserId, } from './user-helpers.js';
|
|
3
5
|
describe('normalizeXhsUserId', () => {
|
|
4
6
|
it('extracts the profile id from a full Xiaohongshu URL', () => {
|
|
@@ -117,3 +119,42 @@ describe('extractXhsUserNotes', () => {
|
|
|
117
119
|
expect(rows[0]?.url).toBe('https://www.rednote.com/user/profile/user-red/note-red?xsec_token=tok&xsec_source=pc_user');
|
|
118
120
|
});
|
|
119
121
|
});
|
|
122
|
+
|
|
123
|
+
describe('assertReadableUserSnapshot', () => {
|
|
124
|
+
it('accepts an explicit empty notes array from a readable user store', () => {
|
|
125
|
+
expect(() => assertReadableUserSnapshot({
|
|
126
|
+
storePresent: true,
|
|
127
|
+
notesPresent: true,
|
|
128
|
+
pageDataPresent: false,
|
|
129
|
+
noteGroups: [],
|
|
130
|
+
pageData: {},
|
|
131
|
+
})).not.toThrow();
|
|
132
|
+
});
|
|
133
|
+
it('fails typed when the user store is missing instead of treating parser drift as empty', () => {
|
|
134
|
+
expect(() => assertReadableUserSnapshot({
|
|
135
|
+
storePresent: false,
|
|
136
|
+
notesPresent: false,
|
|
137
|
+
pageDataPresent: false,
|
|
138
|
+
noteGroups: [],
|
|
139
|
+
pageData: {},
|
|
140
|
+
})).toThrow(CommandExecutionError);
|
|
141
|
+
});
|
|
142
|
+
it('fails typed when profile metadata exists but the notes array is missing', () => {
|
|
143
|
+
expect(() => assertReadableUserSnapshot({
|
|
144
|
+
storePresent: true,
|
|
145
|
+
notesPresent: false,
|
|
146
|
+
pageDataPresent: true,
|
|
147
|
+
noteGroups: [],
|
|
148
|
+
pageData: { user: { nickname: 'Alice' } },
|
|
149
|
+
})).toThrow(CommandExecutionError);
|
|
150
|
+
});
|
|
151
|
+
it('fails typed when notesPresent metadata and cloned noteGroups disagree', () => {
|
|
152
|
+
expect(() => assertReadableUserSnapshot({
|
|
153
|
+
storePresent: true,
|
|
154
|
+
notesPresent: true,
|
|
155
|
+
pageDataPresent: false,
|
|
156
|
+
noteGroups: null,
|
|
157
|
+
pageData: {},
|
|
158
|
+
})).toThrow(CommandExecutionError);
|
|
159
|
+
});
|
|
160
|
+
});
|
package/clis/xiaohongshu/user.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { extractXhsUserNotes, normalizeXhsUserId } from './user-helpers.js';
|
|
3
4
|
/**
|
|
4
5
|
* Host-agnostic IIFE that snapshots the user profile's Pinia store. Exported
|
|
@@ -14,16 +15,33 @@ export const USER_SNAPSHOT_JS = `
|
|
|
14
15
|
}
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
const userStore = window.__INITIAL_STATE__?.user
|
|
18
|
+
const userStore = window.__INITIAL_STATE__?.user;
|
|
19
|
+
const hasUserStore = Boolean(userStore && typeof userStore === 'object');
|
|
20
|
+
const rawNotes = hasUserStore ? (userStore.notes?._value || userStore.notes) : undefined;
|
|
21
|
+
const rawPageData = hasUserStore ? (userStore.userPageData?._value || userStore.userPageData) : undefined;
|
|
18
22
|
return {
|
|
19
|
-
noteGroups: safeClone(
|
|
20
|
-
pageData: safeClone(
|
|
23
|
+
noteGroups: safeClone(rawNotes || []),
|
|
24
|
+
pageData: safeClone(rawPageData || {}),
|
|
25
|
+
storePresent: hasUserStore,
|
|
26
|
+
notesPresent: Array.isArray(rawNotes),
|
|
27
|
+
pageDataPresent: Boolean(rawPageData && typeof rawPageData === 'object' && Object.keys(rawPageData).length > 0),
|
|
21
28
|
};
|
|
22
29
|
})()
|
|
23
30
|
`;
|
|
24
31
|
async function readUserSnapshot(page) {
|
|
25
32
|
return await page.evaluate(USER_SNAPSHOT_JS);
|
|
26
33
|
}
|
|
34
|
+
export function assertReadableUserSnapshot(snapshot) {
|
|
35
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
36
|
+
throw new CommandExecutionError('Malformed Xiaohongshu user snapshot');
|
|
37
|
+
}
|
|
38
|
+
if (snapshot.storePresent !== true) {
|
|
39
|
+
throw new CommandExecutionError('Malformed Xiaohongshu user snapshot: user store was not found');
|
|
40
|
+
}
|
|
41
|
+
if (snapshot.notesPresent !== true || !Array.isArray(snapshot.noteGroups)) {
|
|
42
|
+
throw new CommandExecutionError('Malformed Xiaohongshu user snapshot: notes array was not found');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
27
45
|
export const command = cli({
|
|
28
46
|
site: 'xiaohongshu',
|
|
29
47
|
name: 'user',
|
|
@@ -43,12 +61,14 @@ export const command = cli({
|
|
|
43
61
|
const limit = Math.max(1, Number(kwargs.limit ?? 15));
|
|
44
62
|
await page.goto(`https://www.xiaohongshu.com/user/profile/${userId}`);
|
|
45
63
|
let snapshot = await readUserSnapshot(page);
|
|
64
|
+
assertReadableUserSnapshot(snapshot);
|
|
46
65
|
let results = extractXhsUserNotes(snapshot ?? {}, userId);
|
|
47
66
|
let previousCount = results.length;
|
|
48
67
|
for (let i = 0; results.length < limit && i < 4; i += 1) {
|
|
49
68
|
await page.autoScroll({ times: 1, delayMs: 1500 });
|
|
50
69
|
await page.wait(1);
|
|
51
70
|
snapshot = await readUserSnapshot(page);
|
|
71
|
+
assertReadableUserSnapshot(snapshot);
|
|
52
72
|
const nextResults = extractXhsUserNotes(snapshot ?? {}, userId);
|
|
53
73
|
if (nextResults.length <= previousCount)
|
|
54
74
|
break;
|
|
@@ -56,7 +76,10 @@ export const command = cli({
|
|
|
56
76
|
previousCount = nextResults.length;
|
|
57
77
|
}
|
|
58
78
|
if (results.length === 0) {
|
|
59
|
-
|
|
79
|
+
// 与 bilibili subtitle 同模式:作者无公开内容是合法 empty 数据条件
|
|
80
|
+
// (销号 / 私密号 / 全删笔记),不是 fetch 失败。下游应识别 code
|
|
81
|
+
// EMPTY_RESULT 跳过 rate-limit 启发式、不计入 softFail 阈值。
|
|
82
|
+
throw new EmptyResultError('xiaohongshu user', '该用户没有公开笔记(可能销号 / 私密 / 全部删除)。');
|
|
60
83
|
}
|
|
61
84
|
return results.slice(0, limit);
|
|
62
85
|
},
|
|
@@ -45,7 +45,7 @@ cli({
|
|
|
45
45
|
});
|
|
46
46
|
return [{
|
|
47
47
|
title,
|
|
48
|
-
podcast: ep.podcast?.title || '
|
|
48
|
+
podcast: ep.podcast?.title || '',
|
|
49
49
|
status: result.success ? 'success' : 'failed',
|
|
50
50
|
size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
|
|
51
51
|
file: result.success ? destPath : '-',
|
|
@@ -67,7 +67,7 @@ cli({
|
|
|
67
67
|
}
|
|
68
68
|
return [{
|
|
69
69
|
title: episode.title || 'episode',
|
|
70
|
-
podcast: episode.podcast?.title || '
|
|
70
|
+
podcast: episode.podcast?.title || '',
|
|
71
71
|
status: 'success',
|
|
72
72
|
segments: kwargs.text === false ? '-' : String(segmentCount),
|
|
73
73
|
json_file: kwargs.json === false ? '-' : jsonPath,
|