@skills-store/rednote 0.1.10 → 0.1.12
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 +2 -1
- package/dist/rednote/env.js +19 -6
- package/dist/rednote/getFeedDetail.js +234 -103
- package/dist/rednote/getProfile.js +47 -32
- package/dist/rednote/home.js +11 -7
- package/dist/rednote/index.js +9 -8
- package/dist/rednote/interact.js +4 -4
- package/dist/rednote/login.js +81 -4
- package/dist/rednote/output-format.js +75 -1
- package/dist/rednote/publish.js +38 -26
- package/dist/rednote/search.js +31 -7
- package/package.json +3 -3
package/dist/rednote/publish.js
CHANGED
|
@@ -34,24 +34,36 @@ function printPublishHelp() {
|
|
|
34
34
|
process.stdout.write(`rednote publish
|
|
35
35
|
|
|
36
36
|
Usage:
|
|
37
|
-
npx -y @skills-store/rednote publish --type video --video ./video.mp4 --title
|
|
38
|
-
npx -y @skills-store/rednote publish --type image --image ./1.jpg --image ./2.jpg --title
|
|
39
|
-
npx -y @skills-store/rednote publish --type article --title
|
|
37
|
+
npx -y @skills-store/rednote publish --type video --video ./video.mp4 --title "Video title" --content "Video description" [--tag fashion] [--tag ootd] [--publish] [--instance NAME]
|
|
38
|
+
npx -y @skills-store/rednote publish --type image --image ./1.jpg --image ./2.jpg --title "Image title" --content "Image description" [--tag travel] [--publish] [--instance NAME]
|
|
39
|
+
npx -y @skills-store/rednote publish --type article --title "Article title" --content '# Heading\n\nBody content' [--publish] [--instance NAME]
|
|
40
40
|
node --experimental-strip-types ./scripts/rednote/publish.ts ...
|
|
41
41
|
bun ./scripts/rednote/publish.ts ...
|
|
42
42
|
|
|
43
43
|
Options:
|
|
44
44
|
--instance NAME Optional. Defaults to the saved lastConnect instance
|
|
45
|
-
--type TYPE
|
|
46
|
-
--title TEXT Required.
|
|
47
|
-
--content TEXT
|
|
48
|
-
--tag TEXT
|
|
49
|
-
--video PATH
|
|
50
|
-
--image PATH
|
|
51
|
-
--publish
|
|
45
|
+
--type TYPE Optional. video | image | article. If omitted, the type is inferred from the provided assets
|
|
46
|
+
--title TEXT Required. Post title
|
|
47
|
+
--content TEXT Required. Description for video/image posts, or Markdown content for article posts
|
|
48
|
+
--tag TEXT Optional. Repeat to provide multiple tags, for example: --tag fashion --tag OOTD
|
|
49
|
+
--video PATH Required for video posts. Only one video file is accepted
|
|
50
|
+
--image PATH Required for image posts. Repeat to provide multiple images, up to ${MAX_IMAGE_COUNT}; the first image becomes the cover
|
|
51
|
+
--publish Publish immediately. If omitted, the content is saved as a draft
|
|
52
52
|
-h, --help Show this help
|
|
53
53
|
`);
|
|
54
54
|
}
|
|
55
|
+
function describePublishType(type) {
|
|
56
|
+
if (type === 'video') {
|
|
57
|
+
return 'Upload Video';
|
|
58
|
+
}
|
|
59
|
+
if (type === 'image') {
|
|
60
|
+
return 'Upload Image Post';
|
|
61
|
+
}
|
|
62
|
+
return 'Write Article';
|
|
63
|
+
}
|
|
64
|
+
function describePublishAction(draft) {
|
|
65
|
+
return draft ? 'Save Draft and Leave' : 'Publish';
|
|
66
|
+
}
|
|
55
67
|
function isCreatorHomeUrl(url) {
|
|
56
68
|
return url === CREATOR_HOME_URL || url.startsWith(`${CREATOR_HOME_URL}?`) || url.startsWith(`${CREATOR_HOME_URL}#`);
|
|
57
69
|
}
|
|
@@ -313,13 +325,13 @@ async function openPublishComposer(page, type) {
|
|
|
313
325
|
const publishNoteButton = page.locator(PUBLISH_NOTE_BUTTON_SELECTOR).filter({
|
|
314
326
|
hasText: PUBLISH_NOTE_BUTTON_TEXT
|
|
315
327
|
});
|
|
316
|
-
const visiblePublishNoteButton = await requireVisibleLocator(publishNoteButton, '
|
|
328
|
+
const visiblePublishNoteButton = await requireVisibleLocator(publishNoteButton, 'Could not find the Publish Note button. Make sure the Creator Services home page is open.', 15_000);
|
|
317
329
|
await visiblePublishNoteButton.click();
|
|
318
330
|
const publishTypeTabText = resolvePublishTypeTabText(type);
|
|
319
331
|
const publishTypeTab = page.locator(PUBLISH_TYPE_TAB_SELECTOR).filter({
|
|
320
332
|
hasText: publishTypeTabText
|
|
321
333
|
}).last();
|
|
322
|
-
const visiblePublishTypeTab = await requireVisibleLocator(publishTypeTab,
|
|
334
|
+
const visiblePublishTypeTab = await requireVisibleLocator(publishTypeTab, `Could not find the ${describePublishType(type)} entry. Make sure the Creator Services page finished loading.`, 15_000);
|
|
323
335
|
await visiblePublishTypeTab.click();
|
|
324
336
|
await page.waitForTimeout(300);
|
|
325
337
|
}
|
|
@@ -327,7 +339,7 @@ async function uploadVideoFile(page, videoPath) {
|
|
|
327
339
|
const uploadVideoButton = page.locator(VIDEO_UPLOAD_BUTTON_SELECTOR).filter({
|
|
328
340
|
hasText: VIDEO_UPLOAD_BUTTON_TEXT
|
|
329
341
|
});
|
|
330
|
-
const visibleUploadVideoButton = await requireVisibleLocator(uploadVideoButton, '
|
|
342
|
+
const visibleUploadVideoButton = await requireVisibleLocator(uploadVideoButton, 'Could not find the Upload Video button. Make sure the video publishing page is open.', 15_000);
|
|
331
343
|
const fileChooserPromise = page.waitForEvent('filechooser', {
|
|
332
344
|
timeout: 3_000
|
|
333
345
|
}).catch(()=>null);
|
|
@@ -339,7 +351,7 @@ async function uploadVideoFile(page, videoPath) {
|
|
|
339
351
|
}
|
|
340
352
|
const videoFileInput = page.locator(`${VIDEO_FILE_INPUT_SELECTOR}[accept*="video"], ${VIDEO_FILE_INPUT_SELECTOR}`);
|
|
341
353
|
if (await videoFileInput.count() === 0) {
|
|
342
|
-
throw new Error('
|
|
354
|
+
throw new Error('Could not find the video file input. Make sure the upload component finished loading.');
|
|
343
355
|
}
|
|
344
356
|
await videoFileInput.first().setInputFiles(videoPath);
|
|
345
357
|
}
|
|
@@ -347,7 +359,7 @@ async function uploadImageFiles(page, imagePaths) {
|
|
|
347
359
|
const uploadImageButton = page.locator(IMAGE_UPLOAD_BUTTON_SELECTOR).filter({
|
|
348
360
|
hasText: IMAGE_UPLOAD_BUTTON_TEXT
|
|
349
361
|
});
|
|
350
|
-
const visibleUploadImageButton = await requireVisibleLocator(uploadImageButton, '
|
|
362
|
+
const visibleUploadImageButton = await requireVisibleLocator(uploadImageButton, 'Could not find the Upload Image button. Make sure the image publishing page is open.', 15_000);
|
|
351
363
|
const fileChooserPromise = page.waitForEvent('filechooser', {
|
|
352
364
|
timeout: 3_000
|
|
353
365
|
}).catch(()=>null);
|
|
@@ -359,13 +371,13 @@ async function uploadImageFiles(page, imagePaths) {
|
|
|
359
371
|
}
|
|
360
372
|
const imageFileInput = page.locator(`${IMAGE_FILE_INPUT_SELECTOR}[accept*="image"], ${IMAGE_FILE_INPUT_SELECTOR}`);
|
|
361
373
|
if (await imageFileInput.count() === 0) {
|
|
362
|
-
throw new Error('
|
|
374
|
+
throw new Error('Could not find the image file input. Make sure the upload component finished loading.');
|
|
363
375
|
}
|
|
364
376
|
await imageFileInput.first().setInputFiles(imagePaths);
|
|
365
377
|
}
|
|
366
378
|
async function waitForImagePublishPage(page) {
|
|
367
379
|
const imagePublishPage = page.locator(IMAGE_PUBLISH_PAGE_SELECTOR);
|
|
368
|
-
const visibleImagePublishPage = await requireVisibleLocator(imagePublishPage, '
|
|
380
|
+
const visibleImagePublishPage = await requireVisibleLocator(imagePublishPage, 'The image publishing page did not appear. Make sure the image upload completed successfully.', 30_000);
|
|
369
381
|
await visibleImagePublishPage.waitFor({
|
|
370
382
|
state: 'visible',
|
|
371
383
|
timeout: 30_000
|
|
@@ -373,13 +385,13 @@ async function waitForImagePublishPage(page) {
|
|
|
373
385
|
}
|
|
374
386
|
async function fillImageTitle(page, title) {
|
|
375
387
|
const imageTitleInput = page.locator(IMAGE_TITLE_INPUT_SELECTOR);
|
|
376
|
-
const visibleImageTitleInput = await requireVisibleLocator(imageTitleInput, '
|
|
388
|
+
const visibleImageTitleInput = await requireVisibleLocator(imageTitleInput, 'Could not find the image post title input. Make sure the image publishing page finished loading.', 15_000);
|
|
377
389
|
await visibleImageTitleInput.fill(title);
|
|
378
390
|
await page.waitForTimeout(200);
|
|
379
391
|
}
|
|
380
392
|
async function fillImageContent(page, content) {
|
|
381
393
|
const imageContentEditor = page.locator(IMAGE_CONTENT_EDITOR_SELECTOR);
|
|
382
|
-
const visibleImageContentEditor = await requireVisibleLocator(imageContentEditor, '
|
|
394
|
+
const visibleImageContentEditor = await requireVisibleLocator(imageContentEditor, 'Could not find the image post content editor. Make sure the image publishing page finished loading.', 15_000);
|
|
383
395
|
await visibleImageContentEditor.fill(content);
|
|
384
396
|
await page.waitForTimeout(200);
|
|
385
397
|
}
|
|
@@ -387,19 +399,19 @@ async function openArticleEditor(page) {
|
|
|
387
399
|
const articleNewButton = page.locator(ARTICLE_NEW_BUTTON_SELECTOR).filter({
|
|
388
400
|
hasText: ARTICLE_NEW_BUTTON_TEXT
|
|
389
401
|
});
|
|
390
|
-
const visibleArticleNewButton = await requireVisibleLocator(articleNewButton, '
|
|
402
|
+
const visibleArticleNewButton = await requireVisibleLocator(articleNewButton, 'Could not find the New Creation button. Make sure the article publishing page is open.', 15_000);
|
|
391
403
|
await visibleArticleNewButton.click();
|
|
392
404
|
await page.waitForTimeout(300);
|
|
393
405
|
}
|
|
394
406
|
async function fillArticleTitle(page, title) {
|
|
395
407
|
const articleTitleInput = page.locator(ARTICLE_TITLE_INPUT_SELECTOR);
|
|
396
|
-
const visibleArticleTitleInput = await requireVisibleLocator(articleTitleInput, '
|
|
408
|
+
const visibleArticleTitleInput = await requireVisibleLocator(articleTitleInput, 'Could not find the article title input. Make sure the article editor is open.', 15_000);
|
|
397
409
|
await visibleArticleTitleInput.fill(title);
|
|
398
410
|
await page.waitForTimeout(200);
|
|
399
411
|
}
|
|
400
412
|
async function fillArticleContent(page, content) {
|
|
401
413
|
const articleContentEditor = page.locator(ARTICLE_CONTENT_EDITOR_SELECTOR);
|
|
402
|
-
const visibleArticleContentEditor = await requireVisibleLocator(articleContentEditor, '
|
|
414
|
+
const visibleArticleContentEditor = await requireVisibleLocator(articleContentEditor, 'Could not find the article content editor. Make sure the article editor is open.', 15_000);
|
|
403
415
|
await visibleArticleContentEditor.fill(content);
|
|
404
416
|
await page.waitForTimeout(200);
|
|
405
417
|
}
|
|
@@ -427,7 +439,7 @@ async function finalizePublish(page, draft) {
|
|
|
427
439
|
const publishActionButton = page.locator(PUBLISH_ACTION_BUTTON_SELECTOR).filter({
|
|
428
440
|
hasText: buttonText
|
|
429
441
|
});
|
|
430
|
-
const visiblePublishActionButton = await requireVisibleLocator(publishActionButton,
|
|
442
|
+
const visiblePublishActionButton = await requireVisibleLocator(publishActionButton, `Could not find the ${describePublishAction(draft)} button. Make sure the publish page finished loading.`, 15_000);
|
|
431
443
|
await visiblePublishActionButton.click();
|
|
432
444
|
await page.waitForTimeout(500);
|
|
433
445
|
}
|
|
@@ -442,7 +454,7 @@ export async function openRednotePublish(session, payload) {
|
|
|
442
454
|
const creatorServiceLink = resolved.page.locator(CREATOR_SERVICE_SELECTOR).filter({
|
|
443
455
|
hasText: '创作服务'
|
|
444
456
|
});
|
|
445
|
-
const visibleCreatorServiceLink = await requireVisibleLocator(creatorServiceLink, '
|
|
457
|
+
const visibleCreatorServiceLink = await requireVisibleLocator(creatorServiceLink, 'Could not find the Creator Services entry. Open the Xiaohongshu home page and make sure the account is logged in.');
|
|
446
458
|
const popupPromise = session.browserContext.waitForEvent('page', {
|
|
447
459
|
timeout: 3_000
|
|
448
460
|
}).catch(()=>null);
|
|
@@ -454,7 +466,7 @@ export async function openRednotePublish(session, payload) {
|
|
|
454
466
|
} catch {
|
|
455
467
|
const existingCreatorHomePage = getSessionPages(session).find((page)=>isCreatorHomeUrl(page.url()));
|
|
456
468
|
if (!existingCreatorHomePage) {
|
|
457
|
-
throw new Error(
|
|
469
|
+
throw new Error(`After clicking Creator Services, the page did not navigate to ${CREATOR_HOME_URL}`);
|
|
458
470
|
}
|
|
459
471
|
targetPage = existingCreatorHomePage;
|
|
460
472
|
openedInNewPage = targetPage !== resolved.page;
|
|
@@ -467,7 +479,7 @@ export async function openRednotePublish(session, payload) {
|
|
|
467
479
|
await finalizePublish(targetPage, payload.draft);
|
|
468
480
|
return {
|
|
469
481
|
ok: true,
|
|
470
|
-
message: payload.draft ? '
|
|
482
|
+
message: payload.draft ? 'Publishing page actions completed and Save Draft and Leave was clicked. The content was saved as a draft.' : 'Publishing page actions completed and Publish was clicked.'
|
|
471
483
|
};
|
|
472
484
|
}
|
|
473
485
|
export async function runPublishCommand(values) {
|
package/dist/rednote/search.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { runCli } from '../utils/browser-cli.js';
|
|
3
3
|
import { resolveStatusTarget } from './status.js';
|
|
4
4
|
import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
|
|
5
|
-
import { parseOutputCliArgs, renderPostsMarkdown, resolveSavePath, writePostsJsonl } from './output-format.js';
|
|
5
|
+
import { ensureJsonSavePath, parseOutputCliArgs, renderJsonSaveSummary, renderPostsMarkdown, resolveJsonSavePath, resolveSavePath, writeJsonFile, writePostsJsonl } from './output-format.js';
|
|
6
6
|
export function parseSearchCliArgs(argv) {
|
|
7
7
|
return parseOutputCliArgs(argv, {
|
|
8
8
|
includeKeyword: true
|
|
@@ -20,7 +20,7 @@ Options:
|
|
|
20
20
|
--instance NAME Optional. Defaults to the saved lastConnect instance
|
|
21
21
|
--keyword TEXT Required. Search keyword
|
|
22
22
|
--format FORMAT Output format: md | json. Default: md
|
|
23
|
-
--save [PATH]
|
|
23
|
+
--save [PATH] In markdown mode, saves posts as JSONL and uses a default path when PATH is omitted. In json mode, PATH is required and the posts array is saved as JSON
|
|
24
24
|
-h, --help Show this help
|
|
25
25
|
`);
|
|
26
26
|
}
|
|
@@ -84,6 +84,26 @@ function normalizeSearchPost(item) {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
function toSearchJsonPost(post) {
|
|
88
|
+
return {
|
|
89
|
+
id: post.id,
|
|
90
|
+
modelType: post.modelType,
|
|
91
|
+
xsecToken: post.xsecToken,
|
|
92
|
+
url: post.url,
|
|
93
|
+
displayTitle: post.noteCard.displayTitle,
|
|
94
|
+
cover: post.noteCard.cover.urlDefault,
|
|
95
|
+
userId: post.noteCard.user.userId,
|
|
96
|
+
nickname: post.noteCard.user.nickname,
|
|
97
|
+
avatar: post.noteCard.user.avatar,
|
|
98
|
+
liked: post.noteCard.interactInfo.liked,
|
|
99
|
+
likedCount: post.noteCard.interactInfo.likedCount,
|
|
100
|
+
commentCount: post.noteCard.interactInfo.commentCount,
|
|
101
|
+
collectedCount: post.noteCard.interactInfo.collectedCount,
|
|
102
|
+
sharedCount: post.noteCard.interactInfo.sharedCount,
|
|
103
|
+
imageList: post.noteCard.imageList.map((image)=>image.infoList.find((info)=>typeof info.url === 'string' && info.url)?.url ?? null).filter((url)=>typeof url === 'string' && url.length > 0),
|
|
104
|
+
videoDuration: post.noteCard.video.duration
|
|
105
|
+
};
|
|
106
|
+
}
|
|
87
107
|
async function getOrCreateXiaohongshuPage(session) {
|
|
88
108
|
return session.page;
|
|
89
109
|
}
|
|
@@ -157,6 +177,13 @@ export async function searchRednotePosts(session, keyword) {
|
|
|
157
177
|
};
|
|
158
178
|
}
|
|
159
179
|
function writeSearchOutput(result, values) {
|
|
180
|
+
if (values.format === 'json') {
|
|
181
|
+
const savedPath = resolveJsonSavePath(values.savePath);
|
|
182
|
+
const posts = result.search.posts.map(toSearchJsonPost);
|
|
183
|
+
writeJsonFile(posts, savedPath);
|
|
184
|
+
process.stdout.write(renderJsonSaveSummary(savedPath, posts));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
160
187
|
const posts = result.search.posts;
|
|
161
188
|
let savedPath;
|
|
162
189
|
if (values.saveRequested) {
|
|
@@ -164,10 +191,6 @@ function writeSearchOutput(result, values) {
|
|
|
164
191
|
writePostsJsonl(posts, savedPath);
|
|
165
192
|
result.search.savedPath = savedPath;
|
|
166
193
|
}
|
|
167
|
-
if (values.format === 'json') {
|
|
168
|
-
printJson(result);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
194
|
let markdown = renderPostsMarkdown(posts);
|
|
172
195
|
if (savedPath) {
|
|
173
196
|
markdown = `Saved JSONL: ${savedPath}\n\n${markdown}`;
|
|
@@ -182,6 +205,7 @@ export async function runSearchCommand(values = {
|
|
|
182
205
|
printSearchHelp();
|
|
183
206
|
return;
|
|
184
207
|
}
|
|
208
|
+
ensureJsonSavePath(values.format, values.savePath);
|
|
185
209
|
const keyword = values.keyword?.trim();
|
|
186
210
|
if (!keyword) {
|
|
187
211
|
throw new Error('Missing required option: --keyword');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skills-store/rednote",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|
|
52
52
|
"url": "git+https://github.com/skills-router/skills-store.git",
|
|
53
|
-
"directory": "packages/rednote"
|
|
53
|
+
"directory": "packages/rednote-cli"
|
|
54
54
|
},
|
|
55
|
-
"homepage": "https://github.com/skills-router/skills-store/tree/main/packages/rednote",
|
|
55
|
+
"homepage": "https://github.com/skills-router/skills-store/tree/main/packages/rednote-cli",
|
|
56
56
|
"bugs": {
|
|
57
57
|
"url": "https://github.com/skills-router/skills-store/issues"
|
|
58
58
|
},
|