@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.
@@ -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 标题 --content 描述 [--tag 穿搭] [--tag 日常] [--publish] [--instance NAME]
38
- npx -y @skills-store/rednote publish --type image --image ./1.jpg --image ./2.jpg --title 标题 --content 描述 [--tag 探店] [--publish] [--instance NAME]
39
- npx -y @skills-store/rednote publish --type article --title 标题 --content '# 一级标题\n\n正文' [--publish] [--instance NAME]
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 可选。video | image | article;不传时会按参数自动推断
46
- --title TEXT Required. 发布标题
47
- --content TEXT 必填。视频/图文时为描述,长文时为 Markdown 内容
48
- --tag TEXT 可选。重复传入多个标签,例如 --tag 穿搭 --tag OOTD
49
- --video PATH 视频模式必填。只能传 1 个视频文件
50
- --image PATH 图文模式必填。重复传入多张图片,最多 ${MAX_IMAGE_COUNT} 张,首张为首图
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, '未找到“发布笔记”按钮,请确认已进入创作服务首页。', 15_000);
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, `未找到“${publishTypeTabText}”入口,请确认创作服务页面已正确加载。`, 15_000);
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, '未找到“上传视频”按钮,请确认已进入视频发布页。', 15_000);
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, '未找到“上传图片”按钮,请确认已进入图文发布页。', 15_000);
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, '未等待到图文发布页,请确认图片上传成功。', 30_000);
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, '未找到图文标题输入框,请确认图文发布页已正确加载。', 15_000);
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, '未找到图文正文编辑器,请确认图文发布页已正确加载。', 15_000);
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, '未找到“新的创作”按钮,请确认已进入长文发布页。', 15_000);
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, '未找到长文标题输入框,请确认已进入长文编辑页。', 15_000);
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, '未找到长文正文编辑器,请确认已进入长文编辑页。', 15_000);
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, `未找到“${buttonText}”按钮,请确认发布页已正确加载。`, 15_000);
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(`点击“创作服务”后,未跳转到 ${CREATOR_HOME_URL}`);
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) {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { printJson, runCli } from '../utils/browser-cli.js';
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] Save posts as JSONL. Uses a default path when PATH is omitted
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.10",
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
  },