@skills-store/rednote 0.1.14 → 0.1.16

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.
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { runCli } from '../utils/browser-cli.js';
4
+ import { resolveStatusTarget } from './status.js';
5
+ import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
6
+ import { getProfile, selectProfileOutput, renderProfileMarkdown } from './getProfile.js';
7
+ import { ensureJsonSavePath, renderJsonSaveSummary, resolveJsonSavePath, writeJsonFile } from './output-format.js';
8
+ import { persistProfile } from './persistence.js';
9
+ function printGetMyProfileHelp() {
10
+ process.stdout.write(`rednote get-my-profile
11
+
12
+ Usage:
13
+ npx -y @skills-store/rednote get-my-profile [--instance NAME] [--mode profile|notes] [--max-notes N] [--format md|json] [--save PATH]
14
+ node --experimental-strip-types ./scripts/rednote/getMyProfile.ts --instance NAME [--mode profile|notes] [--max-notes N] [--format md|json] [--save PATH]
15
+ bun ./scripts/rednote/getMyProfile.ts --instance NAME [--mode profile|notes] [--max-notes N] [--format md|json] [--save PATH]
16
+
17
+ Options:
18
+ --instance NAME Optional. Defaults to the saved lastConnect instance
19
+ --mode MODE Optional. profile | notes. Default: profile
20
+ --max-notes N Optional. Max notes to fetch by scrolling. Default: 100
21
+ --format FORMAT Output format: md | json. Default: md
22
+ --save PATH Required when --format json is used. Saves only the selected mode data as JSON
23
+ -h, --help Show this help
24
+ `);
25
+ }
26
+ export function parseGetMyProfileCliArgs(argv) {
27
+ const { values, positionals } = parseArgs({
28
+ args: argv,
29
+ allowPositionals: true,
30
+ strict: false,
31
+ options: {
32
+ instance: {
33
+ type: 'string'
34
+ },
35
+ format: {
36
+ type: 'string'
37
+ },
38
+ mode: {
39
+ type: 'string'
40
+ },
41
+ 'max-notes': {
42
+ type: 'string'
43
+ },
44
+ save: {
45
+ type: 'string'
46
+ },
47
+ help: {
48
+ type: 'boolean',
49
+ short: 'h'
50
+ }
51
+ }
52
+ });
53
+ if (positionals.length > 0) {
54
+ throw new Error(`Unexpected positional arguments: ${positionals.join(' ')}`);
55
+ }
56
+ const format = values.format ?? 'md';
57
+ if (format !== 'md' && format !== 'json') {
58
+ throw new Error(`Invalid --format value: ${String(format)}`);
59
+ }
60
+ const mode = values.mode ?? 'profile';
61
+ if (mode !== 'profile' && mode !== 'notes') {
62
+ throw new Error(`Invalid --mode value: ${String(values.mode)}`);
63
+ }
64
+ const maxNotesValue = values['max-notes'] ?? '100';
65
+ const maxNotes = parseInt(maxNotesValue, 10);
66
+ if (isNaN(maxNotes) || maxNotes < 1) {
67
+ throw new Error(`Invalid --max-notes value: ${maxNotesValue}`);
68
+ }
69
+ return {
70
+ instance: values.instance,
71
+ format,
72
+ mode,
73
+ maxNotes,
74
+ savePath: values.save,
75
+ help: values.help
76
+ };
77
+ }
78
+ function writeMyProfileOutput(result, values) {
79
+ const output = selectProfileOutput(result, values.mode);
80
+ if (values.format === 'json') {
81
+ const savedPath = resolveJsonSavePath(values.savePath);
82
+ writeJsonFile(output, savedPath);
83
+ process.stdout.write(renderJsonSaveSummary(savedPath, output));
84
+ return;
85
+ }
86
+ process.stdout.write(renderProfileMarkdown(result, values.mode));
87
+ }
88
+ async function navigateToMyProfile(page) {
89
+ await page.goto('https://www.xiaohongshu.com/explore', {
90
+ waitUntil: 'domcontentloaded'
91
+ });
92
+ await page.waitForTimeout(2000);
93
+ const userLink = page.locator('.user.side-bar-component').first();
94
+ await userLink.click();
95
+ await page.waitForURL(/\/user\/profile\//, {
96
+ timeout: 10000
97
+ });
98
+ await page.waitForTimeout(1000);
99
+ const profileUrl = page.url();
100
+ const match = profileUrl.match(/\/user\/profile\/([^/?]+)/);
101
+ if (!match) {
102
+ throw new Error(`Failed to extract userId from profile URL: ${profileUrl}`);
103
+ }
104
+ const userId = match[1];
105
+ return {
106
+ userId,
107
+ profileUrl
108
+ };
109
+ }
110
+ export async function runGetMyProfileCommand(values = {
111
+ format: 'md',
112
+ mode: 'profile',
113
+ maxNotes: 100
114
+ }) {
115
+ if (values.help) {
116
+ printGetMyProfileHelp();
117
+ return;
118
+ }
119
+ ensureJsonSavePath(values.format, values.savePath);
120
+ const target = resolveStatusTarget(values.instance);
121
+ const session = await createRednoteSession(target);
122
+ try {
123
+ await ensureRednoteLoggedIn(target, 'fetching my profile', session);
124
+ const { userId, profileUrl } = await navigateToMyProfile(session.page);
125
+ const result = await getProfile(session, profileUrl, userId, values.maxNotes);
126
+ await persistProfile({
127
+ instanceName: target.instanceName,
128
+ result
129
+ });
130
+ writeMyProfileOutput(result, values);
131
+ } finally{
132
+ await disconnectRednoteSession(session);
133
+ }
134
+ }
135
+ async function main() {
136
+ const values = parseGetMyProfileCliArgs(process.argv.slice(2));
137
+ await runGetMyProfileCommand(values);
138
+ }
139
+ runCli(import.meta.url, main);
@@ -3,21 +3,24 @@ import * as cheerio from 'cheerio';
3
3
  import { parseArgs } from 'node:util';
4
4
  import vm from 'node:vm';
5
5
  import { runCli } from '../utils/browser-cli.js';
6
+ import { simulateMouseMove, simulateMouseWheel } from '../utils/mouse-helper.js';
6
7
  import { resolveStatusTarget } from './status.js';
7
8
  import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
8
9
  import { ensureJsonSavePath, renderJsonSaveSummary, renderPostsMarkdown, resolveJsonSavePath, writeJsonFile } from './output-format.js';
10
+ import { persistProfile } from './persistence.js';
9
11
  function printGetProfileHelp() {
10
12
  process.stdout.write(`rednote get-profile
11
13
 
12
14
  Usage:
13
- npx -y @skills-store/rednote get-profile [--instance NAME] --id USER_ID [--mode profile|notes] [--format md|json] [--save PATH]
14
- node --experimental-strip-types ./scripts/rednote/getProfile.ts --instance NAME --id USER_ID [--mode profile|notes] [--format md|json] [--save PATH]
15
- bun ./scripts/rednote/getProfile.ts --instance NAME --id USER_ID [--mode profile|notes] [--format md|json] [--save PATH]
15
+ npx -y @skills-store/rednote get-profile [--instance NAME] --id USER_ID [--mode profile|notes] [--max-notes N] [--format md|json] [--save PATH]
16
+ node --experimental-strip-types ./scripts/rednote/getProfile.ts --instance NAME --id USER_ID [--mode profile|notes] [--max-notes N] [--format md|json] [--save PATH]
17
+ bun ./scripts/rednote/getProfile.ts --instance NAME --id USER_ID [--mode profile|notes] [--max-notes N] [--format md|json] [--save PATH]
16
18
 
17
19
  Options:
18
20
  --instance NAME Optional. Defaults to the saved lastConnect instance
19
21
  --id USER_ID Required. Xiaohongshu profile user id
20
22
  --mode MODE Optional. profile | notes. Default: profile
23
+ --max-notes N Optional. Max notes to fetch by scrolling. Default: 100
21
24
  --format FORMAT Output format: md | json. Default: md
22
25
  --save PATH Required when --format json is used. Saves only the selected mode data as JSON
23
26
  -h, --help Show this help
@@ -41,6 +44,9 @@ export function parseGetProfileCliArgs(argv) {
41
44
  mode: {
42
45
  type: 'string'
43
46
  },
47
+ 'max-notes': {
48
+ type: 'string'
49
+ },
44
50
  save: {
45
51
  type: 'string'
46
52
  },
@@ -61,11 +67,17 @@ export function parseGetProfileCliArgs(argv) {
61
67
  if (mode !== 'profile' && mode !== 'notes') {
62
68
  throw new Error(`Invalid --mode value: ${String(values.mode)}`);
63
69
  }
70
+ const maxNotesValue = values['max-notes'] ?? '100';
71
+ const maxNotes = parseInt(maxNotesValue, 10);
72
+ if (isNaN(maxNotes) || maxNotes < 1) {
73
+ throw new Error(`Invalid --max-notes value: ${maxNotesValue}`);
74
+ }
64
75
  return {
65
76
  instance: values.instance,
66
77
  id: values.id,
67
78
  format,
68
79
  mode,
80
+ maxNotes,
69
81
  savePath: values.save,
70
82
  help: values.help
71
83
  };
@@ -228,67 +240,164 @@ function renderProfileUserMarkdown(result) {
228
240
  lines.push(`- Tags: ${user.tags.length > 0 ? user.tags.map((tag)=>`#${tag}`).join(' ') : ''}`);
229
241
  return `${lines.join('\n')}\n`;
230
242
  }
231
- function selectProfileOutput(result, mode) {
243
+ export function selectProfileOutput(result, mode) {
232
244
  return mode === 'notes' ? result.profile.notes : result.profile.user;
233
245
  }
234
- function renderProfileMarkdown(result, mode) {
246
+ export function renderProfileMarkdown(result, mode) {
235
247
  if (mode === 'notes') {
236
248
  return renderPostsMarkdown(result.profile.notes);
237
249
  }
238
250
  return renderProfileUserMarkdown(result);
239
251
  }
240
- async function captureProfile(page, targetUrl) {
252
+ const NOTES_CONTAINER_SELECTOR = '.feeds-tab-container';
253
+ const NOTES_SCROLL_TIMEOUT_MS = 60_000;
254
+ const NOTES_SCROLL_IDLE_LIMIT = 4;
255
+ async function scrollNotesContainer(page, maxNotes, getNoteCount) {
256
+ const container = page.locator(NOTES_CONTAINER_SELECTOR).first();
257
+ const visible = await container.isVisible().catch(()=>false);
258
+ if (!visible) {
259
+ return;
260
+ }
261
+ await container.scrollIntoViewIfNeeded().catch(()=>{});
262
+ await simulateMouseMove(page, {
263
+ locator: container,
264
+ settleMs: 100
265
+ }).catch(()=>{});
266
+ const getMetrics = async ()=>await container.evaluate((element)=>{
267
+ const htmlElement = element;
268
+ const atBottom = htmlElement.scrollTop + htmlElement.clientHeight >= htmlElement.scrollHeight - 8;
269
+ return {
270
+ scrollTop: htmlElement.scrollTop,
271
+ scrollHeight: htmlElement.scrollHeight,
272
+ clientHeight: htmlElement.clientHeight,
273
+ atBottom
274
+ };
275
+ }).catch(()=>null);
276
+ const deadline = Date.now() + NOTES_SCROLL_TIMEOUT_MS;
277
+ let idleRounds = 0;
278
+ while(Date.now() < deadline){
279
+ if (getNoteCount() >= maxNotes) {
280
+ return;
281
+ }
282
+ const beforeMetrics = await getMetrics();
283
+ if (!beforeMetrics) {
284
+ return;
285
+ }
286
+ const beforeCount = getNoteCount();
287
+ const delta = Math.max(Math.floor(beforeMetrics.clientHeight * 0.85), 480);
288
+ await simulateMouseWheel(page, {
289
+ locator: container,
290
+ deltaY: delta,
291
+ moveBeforeScroll: false,
292
+ settleMs: 900
293
+ }).catch(()=>{});
294
+ const afterMetrics = await getMetrics();
295
+ await page.waitForTimeout(400);
296
+ const afterCount = getNoteCount();
297
+ const countChanged = afterCount > beforeCount;
298
+ const scrollMoved = Boolean(afterMetrics) && afterMetrics.scrollTop > beforeMetrics.scrollTop;
299
+ const reachedBottom = Boolean(afterMetrics?.atBottom);
300
+ if (countChanged || scrollMoved) {
301
+ idleRounds = 0;
302
+ continue;
303
+ }
304
+ idleRounds += 1;
305
+ if (reachedBottom && idleRounds >= 2 || idleRounds >= NOTES_SCROLL_IDLE_LIMIT) {
306
+ return;
307
+ }
308
+ }
309
+ }
310
+ async function captureProfile(page, targetUrl, maxNotes) {
241
311
  let userPageData = null;
242
- let notes = null;
312
+ const notesMap = new Map();
243
313
  const handleResponse = async (response)=>{
244
314
  try {
245
315
  const url = new URL(response.url());
246
- if (response.status() !== 200 || !url.href.includes('/user/profile/')) {
316
+ if (response.status() !== 200 || !(url.href.includes('/user/profile/') || url.href.includes('/api/sns/web/v1/user_posted'))) {
247
317
  return;
248
318
  }
249
- const html = await response.text();
250
- const $ = cheerio.load(html);
251
- $('script').each((_, element)=>{
252
- const scriptContent = $(element).html();
253
- if (!scriptContent?.includes('window.__INITIAL_STATE__')) {
254
- return;
319
+ if (url.href.includes('/user/profile/')) {
320
+ const html = await response.text();
321
+ const $ = cheerio.load(html);
322
+ $('script').each((_, element)=>{
323
+ const scriptContent = $(element).html();
324
+ if (!scriptContent?.includes('window.__INITIAL_STATE__')) {
325
+ return;
326
+ }
327
+ const scriptText = scriptContent.substring(scriptContent.indexOf('=') + 1);
328
+ const sandbox = {};
329
+ vm.createContext(sandbox);
330
+ vm.runInContext(`var info = ${scriptText}`, sandbox);
331
+ userPageData = sandbox.info?.user?.userPageData ?? userPageData;
332
+ const notesData = sandbox.info?.user?.notes;
333
+ if (Array.isArray(notesData)) {
334
+ for (const note of notesData){
335
+ if (!Array.isArray(notesData)) {
336
+ const noteId = note?.id ?? note?.noteId ?? note?.note_id;
337
+ if (noteId && !notesMap.has(noteId)) {
338
+ notesMap.set(noteId, note);
339
+ }
340
+ } else {
341
+ for (const _note of note){
342
+ const noteId = _note?.id ?? _note?.noteId ?? _note?.note_id;
343
+ if (noteId && !notesMap.has(noteId)) {
344
+ notesMap.set(noteId, _note);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ });
351
+ }
352
+ if (url.href.includes('/api/sns/web/v1/user_posted')) {
353
+ const body = await response.json();
354
+ if (body.code == 0 && body.data?.notes) {
355
+ if (Array.isArray(body.data?.notes)) {
356
+ for (const note of body.data?.notes){
357
+ const noteId = note?.id ?? note?.noteId ?? note?.note_id;
358
+ if (noteId && !notesMap.has(noteId)) {
359
+ notesMap.set(noteId, note);
360
+ }
361
+ }
362
+ }
255
363
  }
256
- const scriptText = scriptContent.substring(scriptContent.indexOf('=') + 1);
257
- const sandbox = {};
258
- vm.createContext(sandbox);
259
- vm.runInContext(`var info = ${scriptText}`, sandbox);
260
- userPageData = sandbox.info?.user?.userPageData ?? userPageData;
261
- notes = sandbox.info?.user?.notes ?? notes;
262
- });
364
+ }
263
365
  } catch {}
264
366
  };
265
367
  page.on('response', handleResponse);
266
368
  try {
267
- await page.goto(targetUrl, {
268
- waitUntil: 'domcontentloaded'
269
- });
369
+ if (targetUrl !== page.url()) {
370
+ await page.goto(targetUrl, {
371
+ waitUntil: 'domcontentloaded'
372
+ });
373
+ } else {}
270
374
  const deadline = Date.now() + 15_000;
271
375
  while(Date.now() < deadline){
272
- if (userPageData || notes) {
376
+ if (userPageData || notesMap.size > 0) {
273
377
  break;
274
378
  }
275
379
  await page.waitForTimeout(200);
276
380
  }
277
- if (!userPageData && !notes) {
381
+ if (!userPageData && notesMap.size === 0) {
278
382
  throw new Error(`Failed to capture profile detail: ${targetUrl}`);
279
383
  }
384
+ if (notesMap.size < maxNotes) {
385
+ await scrollNotesContainer(page, maxNotes, ()=>notesMap.size);
386
+ }
280
387
  return {
281
388
  userPageData,
282
- notes
389
+ notes: [
390
+ ...notesMap.values()
391
+ ]
283
392
  };
284
393
  } finally{
285
394
  page.off('response', handleResponse);
286
395
  }
287
396
  }
288
- export async function getProfile(session, url, userId) {
397
+ export async function getProfile(session, url, userId, maxNotes = 100) {
289
398
  validateProfileUrl(url);
290
399
  const page = await getOrCreateXiaohongshuPage(session);
291
- const captured = await captureProfile(page, url);
400
+ const captured = await captureProfile(page, url, maxNotes);
292
401
  return {
293
402
  ok: true,
294
403
  profile: {
@@ -315,7 +424,8 @@ function writeProfileOutput(result, values) {
315
424
  }
316
425
  export async function runGetProfileCommand(values = {
317
426
  format: 'md',
318
- mode: 'profile'
427
+ mode: 'profile',
428
+ maxNotes: 100
319
429
  }) {
320
430
  if (values.help) {
321
431
  printGetProfileHelp();
@@ -330,7 +440,11 @@ export async function runGetProfileCommand(values = {
330
440
  const session = await createRednoteSession(target);
331
441
  try {
332
442
  await ensureRednoteLoggedIn(target, 'fetching profile', session);
333
- const result = await getProfile(session, buildProfileUrl(normalizedUserId), normalizedUserId);
443
+ const result = await getProfile(session, buildProfileUrl(normalizedUserId), normalizedUserId, values.maxNotes);
444
+ await persistProfile({
445
+ instanceName: target.instanceName,
446
+ result
447
+ });
334
448
  writeProfileOutput(result, values);
335
449
  } finally{
336
450
  await disconnectRednoteSession(session);
@@ -11,11 +11,12 @@ Commands:
11
11
  check-login [--instance NAME]
12
12
  login [--instance NAME]
13
13
  publish [--instance NAME]
14
- interact [--instance NAME] --url URL [--like] [--collect] [--comment TEXT]
14
+ interact [--instance NAME] [--id ID | --url URL] [--like] [--collect] [--comment TEXT]
15
15
  home [--instance NAME] [--format md|json] [--save [PATH]]
16
16
  search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
17
17
  get-feed-detail [--instance NAME] --url URL [--url URL] [--comments [COUNT]] [--format md|json] [--save PATH]
18
18
  get-profile [--instance NAME] --id USER_ID [--mode profile|notes] [--format md|json] [--save PATH]
19
+ get-my-profile [--instance NAME] [--mode profile|notes] [--format md|json] [--save PATH]
19
20
 
20
21
  Examples:
21
22
  npx -y @skills-store/rednote browser list
@@ -25,7 +26,7 @@ Examples:
25
26
  npx -y @skills-store/rednote status --instance seller-main
26
27
  npx -y @skills-store/rednote login --instance seller-main
27
28
  npx -y @skills-store/rednote publish --instance seller-main --type video --video ./note.mp4 --title "Video title" --content "Video description"
28
- npx -y @skills-store/rednote interact --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect --comment "Great post"
29
+ npx -y @skills-store/rednote interact --instance seller-main --id NOTE_ID --like --collect --comment "Great post"
29
30
  npx -y @skills-store/rednote home --instance seller-main --format md --save
30
31
  npx -y @skills-store/rednote search --instance seller-main --keyword skincare --format json --save ./output/search.json
31
32
  npx -y @skills-store/rednote get-feed-detail --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --comments 100 --format json --save ./output/feed-detail.json
@@ -127,6 +128,11 @@ export async function runRednoteCli(argv = process.argv.slice(2)) {
127
128
  await runGetProfileCommand(parseGetProfileCliArgs(commandArgv));
128
129
  return;
129
130
  }
131
+ if (command === 'get-my-profile') {
132
+ const { parseGetMyProfileCliArgs, runGetMyProfileCommand } = await import('./getMyProfile.js');
133
+ await runGetMyProfileCommand(parseGetMyProfileCliArgs(commandArgv));
134
+ return;
135
+ }
130
136
  throw new Error(`Unknown command: ${command}`);
131
137
  }
132
138
  async function main() {
@@ -4,6 +4,7 @@ import { printJson, runCli } from '../utils/browser-cli.js';
4
4
  import { resolveStatusTarget } from './status.js';
5
5
  import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
6
6
  import { getFeedDetails } from './getFeedDetail.js';
7
+ import { findPersistedPostUrlByRecordId, initializeRednoteDatabase } from './persistence.js';
7
8
  const INTERACT_CONTAINER_SELECTOR = '.interact-container';
8
9
  const LIKE_WRAPPER_SELECTOR = `${INTERACT_CONTAINER_SELECTOR} .like-wrapper`;
9
10
  const COLLECT_WRAPPER_SELECTOR = `${INTERACT_CONTAINER_SELECTOR} .collect-wrapper, ${INTERACT_CONTAINER_SELECTOR} #note-page-collect-board-guide`;
@@ -14,13 +15,14 @@ function printInteractHelp() {
14
15
  process.stdout.write(`rednote interact
15
16
 
16
17
  Usage:
17
- npx -y @skills-store/rednote interact [--instance NAME] --url URL [--like] [--collect] [--comment TEXT]
18
- node --experimental-strip-types ./scripts/rednote/interact.ts --instance NAME --url URL [--like] [--collect] [--comment TEXT]
19
- bun ./scripts/rednote/interact.ts --instance NAME --url URL [--like] [--collect] [--comment TEXT]
18
+ npx -y @skills-store/rednote interact [--instance NAME] [--id ID | --url URL] [--like] [--collect] [--comment TEXT]
19
+ node --experimental-strip-types ./scripts/rednote/interact.ts [--instance NAME] [--id ID | --url URL] [--like] [--collect] [--comment TEXT]
20
+ bun ./scripts/rednote/interact.ts [--instance NAME] [--id ID | --url URL] [--like] [--collect] [--comment TEXT]
20
21
 
21
22
  Options:
22
23
  --instance NAME Optional. Defaults to the saved lastConnect instance
23
- --url URL Required. Xiaohongshu explore url
24
+ --id ID Optional. Database record id from home/search output
25
+ --url URL Optional. Xiaohongshu explore url
24
26
  --like Optional. Perform like
25
27
  --collect Optional. Perform collect
26
28
  --comment TEXT Optional. Post comment content
@@ -36,6 +38,9 @@ export function parseInteractCliArgs(argv) {
36
38
  instance: {
37
39
  type: 'string'
38
40
  },
41
+ id: {
42
+ type: 'string'
43
+ },
39
44
  url: {
40
45
  type: 'string'
41
46
  },
@@ -59,6 +64,7 @@ export function parseInteractCliArgs(argv) {
59
64
  }
60
65
  return {
61
66
  instance: values.instance,
67
+ id: values.id,
62
68
  url: values.url,
63
69
  like: values.like,
64
70
  collect: values.collect,
@@ -253,8 +259,8 @@ export async function interactWithFeed(session, url, actions, commentContent) {
253
259
  }
254
260
  const page = await getOrCreateXiaohongshuPage(session);
255
261
  await waitForInteractContainer(page);
256
- let liked = detailItem.note.interactInfo.liked === true;
257
- let collected = detailItem.note.interactInfo.collected === true;
262
+ let liked = detailItem.note.liked === true;
263
+ let collected = detailItem.note.collected === true;
258
264
  const messages = [];
259
265
  for (const action of actions){
260
266
  if (action === 'like') {
@@ -280,17 +286,34 @@ export async function interactWithFeed(session, url, actions, commentContent) {
280
286
  message: `${messages.join('; ')}: ${url}`
281
287
  };
282
288
  }
289
+ async function resolveInteractUrl(values, instanceName) {
290
+ if (values.id) {
291
+ if (!instanceName) {
292
+ throw new Error('The --id option requires an instance-backed session.');
293
+ }
294
+ const url = await findPersistedPostUrlByRecordId(instanceName, ensureNonEmpty(values.id, '--id'));
295
+ if (!url) {
296
+ throw new Error(`No saved post url found for id: ${values.id}`);
297
+ }
298
+ return url;
299
+ }
300
+ if (values.url) {
301
+ return ensureNonEmpty(values.url, '--url');
302
+ }
303
+ throw new Error('Missing required option: --id or --url');
304
+ }
283
305
  export async function runInteractCommand(values = {}) {
284
306
  if (values.help) {
285
307
  printInteractHelp();
286
308
  return;
287
309
  }
288
- const url = ensureNonEmpty(values.url, '--url');
289
310
  const { actions, commentContent } = resolveInteractActions(values);
311
+ await initializeRednoteDatabase();
290
312
  const target = resolveStatusTarget(values.instance);
291
313
  const session = await createRednoteSession(target);
292
314
  try {
293
315
  await ensureRednoteLoggedIn(target, `performing ${actions.join(', ')} interact`, session);
316
+ const url = await resolveInteractUrl(values, target.instanceName);
294
317
  const result = await interactWithFeed(session, url, actions, commentContent);
295
318
  printJson(result);
296
319
  } finally{
@@ -103,13 +103,11 @@ export async function openRednoteLogin(target, session) {
103
103
  if (!rednoteStatus.needLogin) {
104
104
  return {
105
105
  ok: true,
106
- rednote: {
107
- loginClicked: false,
108
- pageUrl: session.page.url(),
109
- waitingForPhoneLogin: false,
110
- qrCodePath: null,
111
- message: 'The current instance is already logged in. No additional login step is required.'
112
- }
106
+ loginClicked: false,
107
+ pageUrl: session.page.url(),
108
+ waitingForPhoneLogin: false,
109
+ qrCodePath: null,
110
+ message: 'The current instance is already logged in. No additional login step is required.'
113
111
  };
114
112
  }
115
113
  const { page } = await getOrCreateXiaohongshuPage(session);
@@ -119,13 +117,11 @@ export async function openRednoteLogin(target, session) {
119
117
  if (!hasLoginButton) {
120
118
  return {
121
119
  ok: true,
122
- rednote: {
123
- loginClicked: false,
124
- pageUrl: page.url(),
125
- waitingForPhoneLogin: false,
126
- qrCodePath: null,
127
- message: 'No login button was found. The current instance may already be logged in.'
128
- }
120
+ loginClicked: false,
121
+ pageUrl: page.url(),
122
+ waitingForPhoneLogin: false,
123
+ qrCodePath: null,
124
+ message: 'No login button was found. The current instance may already be logged in.'
129
125
  };
130
126
  }
131
127
  await loginButton.first().click({
@@ -136,13 +132,11 @@ export async function openRednoteLogin(target, session) {
136
132
  const qrCodePath = await saveQrCodeImage(page);
137
133
  return {
138
134
  ok: true,
139
- rednote: {
140
- loginClicked: true,
141
- pageUrl: page.url(),
142
- waitingForPhoneLogin: true,
143
- qrCodePath,
144
- message: 'The login button was clicked and the QR code image was exported. Scan the code to finish logging in.'
145
- }
135
+ loginClicked: true,
136
+ pageUrl: page.url(),
137
+ waitingForPhoneLogin: true,
138
+ qrCodePath,
139
+ message: 'The login button was clicked and the QR code image was exported. Scan the code to finish logging in.'
146
140
  };
147
141
  }
148
142
  export async function runLoginCommand(values = {}) {