@skills-store/rednote 0.1.13 → 0.1.15

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 CHANGED
@@ -36,10 +36,10 @@ For most tasks, run commands in this order:
36
36
  rednote env
37
37
  rednote browser create --name seller-main --browser chrome --port 9222
38
38
  rednote browser connect --instance seller-main
39
- rednote login --instance seller-main
40
- rednote status --instance seller-main
41
- rednote search --instance seller-main --keyword 护肤
42
- rednote interact --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect --comment "写得真好"
39
+ rednote login
40
+ rednote status
41
+ rednote search --keyword 护肤
42
+ rednote interact --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect --comment "写得真好"
43
43
  ```
44
44
 
45
45
  ## Commands
@@ -68,7 +68,7 @@ Use `env` first when checking installation, runtime info, or storage paths.
68
68
  ### `status`
69
69
 
70
70
  ```bash
71
- rednote status --instance seller-main
71
+ rednote status
72
72
  ```
73
73
 
74
74
  Use `status` to confirm whether an instance exists, is running, and appears logged in.
@@ -76,7 +76,7 @@ Use `status` to confirm whether an instance exists, is running, and appears logg
76
76
  ### `check-login`
77
77
 
78
78
  ```bash
79
- rednote check-login --instance seller-main
79
+ rednote check-login
80
80
  ```
81
81
 
82
82
  Use `check-login` when you only want to verify whether the session is still valid.
@@ -84,7 +84,7 @@ Use `check-login` when you only want to verify whether the session is still vali
84
84
  ### `login`
85
85
 
86
86
  ```bash
87
- rednote login --instance seller-main
87
+ rednote login
88
88
  ```
89
89
 
90
90
  Use `login` after `browser connect` if the instance is not authenticated yet.
@@ -92,32 +92,59 @@ Use `login` after `browser connect` if the instance is not authenticated yet.
92
92
  ### `home`
93
93
 
94
94
  ```bash
95
- rednote home --instance seller-main --format md --save
95
+ rednote home --format md --save
96
96
  ```
97
97
 
98
98
  Use `home` when you want the current home feed and optionally want to save it to disk.
99
99
 
100
+ The terminal output always uses the compact summary format below, even when `--format json` is selected:
101
+
102
+ ```text
103
+ id=<database nanoid>
104
+ title=<post title>
105
+ like=<liked count>
106
+
107
+ id=...
108
+ title=...
109
+ like=...
110
+ ```
111
+
112
+ Captured home feed posts are upserted into `~/.skills-router/rednote/main.db` (the same path returned by `rednote env`). They are stored in the `rednote_posts` table, and the printed `id` is that table's `nanoid(16)` primary key.
113
+
100
114
  ### `search`
101
115
 
102
116
  ```bash
103
- rednote search --instance seller-main --keyword 护肤
104
- rednote search --instance seller-main --keyword 护肤 --format json --save ./output/search.jsonl
117
+ rednote search --keyword 护肤
118
+ rednote search --keyword 护肤 --format json --save ./output/search.jsonl
105
119
  ```
106
120
 
107
121
  Use `search` for keyword-based note lookup.
108
122
 
123
+ The terminal output always uses the compact summary format below, even when `--format json` is selected:
124
+
125
+ ```text
126
+ id=<database nanoid>
127
+ title=<post title>
128
+ like=<liked count>
129
+ ```
130
+
131
+ Captured search results are also upserted into `~/.skills-router/rednote/main.db` in the `rednote_posts` table. The printed `id` can be passed directly to `get-feed-detail --id`.
132
+
109
133
  ### `get-feed-detail`
110
134
 
111
135
  ```bash
112
- rednote get-feed-detail --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy"
136
+ rednote get-feed-detail --id <nanoid>
137
+ rednote get-feed-detail --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy"
113
138
  ```
114
139
 
115
- Use `get-feed-detail` when you already have a Xiaohongshu note URL.
140
+ Use `get-feed-detail` when you already have a Xiaohongshu note URL, or when you have a database `id` returned by `home` or `search`. With `--id`, the CLI looks up the saved URL from `~/.skills-router/rednote/main.db` and then navigates with that raw URL.
141
+
142
+ Captured note details and comments are also upserted into `~/.skills-router/rednote/main.db` in `rednote_post_details` and `rednote_post_comments`.
116
143
 
117
144
  ### `get-profile`
118
145
 
119
146
  ```bash
120
- rednote get-profile --instance seller-main --id USER_ID
147
+ rednote get-profile --id USER_ID
121
148
  ```
122
149
 
123
150
  Use `get-profile` when you want author or account profile information.
@@ -126,8 +153,8 @@ Use `get-profile` when you want author or account profile information.
126
153
  ### `interact`
127
154
 
128
155
  ```bash
129
- rednote interact --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect
130
- rednote interact --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect --comment "写得真好"
156
+ rednote interact --id <nanoid> --like --collect
157
+ rednote interact --id <nanoid> --like --collect --comment "写得真好"
131
158
  ```
132
159
 
133
160
  Use `interact` when you want the single entrypoint for note operations such as like, collect, and comment in one command. Use `--comment TEXT` for replies; there is no standalone `comment` command.
@@ -137,335 +164,14 @@ Use `interact` when you want the single entrypoint for note operations such as l
137
164
  - `--instance NAME` selects the browser instance for account-scoped commands.
138
165
  - `--format json` is best for scripting.
139
166
  - `--format md` is best for direct reading.
140
- - `--save` is useful for `home` and `search` when you want saved output.
167
+ - `--save` is useful for `home` and `search` when you want the raw post array written to disk.
141
168
  - `--keyword` is required for `search`.
142
- - `--url` is required for `get-feed-detail`.
169
+ - `home` and `search` always print `id/title/like` summaries to stdout; `--format json` only changes the saved file payload.
170
+ - `get-feed-detail` accepts either `--url URL` or `--id ID`.
143
171
  - `--id` is required for `get-profile`.
144
172
  - `--url` is required for `interact`; at least one of `--like`, `--collect`, or `--comment TEXT` must be provided.
145
173
  - replies are sent with `interact --comment TEXT`.
146
174
 
147
- ## JSON success shapes
148
-
149
- Use these shapes as the success model when a command returns JSON.
150
-
151
- ### Shared note item
152
-
153
- `home`, `search`, and `profile.notes` share the normalized `RednotePost` shape:
154
-
155
- ```json
156
- {
157
- "id": "string",
158
- "modelType": "string",
159
- "xsecToken": "string|null",
160
- "url": "string",
161
- "noteCard": {
162
- "type": "string|null",
163
- "displayTitle": "string|null",
164
- "cover": {
165
- "urlDefault": "string|null",
166
- "urlPre": "string|null",
167
- "url": "string|null",
168
- "fileId": "string|null",
169
- "width": "number|null",
170
- "height": "number|null",
171
- "infoList": [{ "imageScene": "string|null", "url": "string|null" }]
172
- },
173
- "user": {
174
- "userId": "string|null",
175
- "nickname": "string|null",
176
- "nickName": "string|null",
177
- "avatar": "string|null",
178
- "xsecToken": "string|null"
179
- },
180
- "interactInfo": {
181
- "liked": "boolean",
182
- "likedCount": "string|null",
183
- "commentCount": "string|null",
184
- "collectedCount": "string|null",
185
- "sharedCount": "string|null"
186
- },
187
- "cornerTagInfo": [{ "type": "string|null", "text": "string|null" }],
188
- "imageList": [{ "width": "number|null", "height": "number|null", "infoList": [{ "imageScene": "string|null", "url": "string|null" }] }],
189
- "video": { "duration": "number|null" }
190
- }
191
- }
192
- ```
193
-
194
- ### `env --format json`
195
-
196
- `env` is the main exception: it returns a raw environment object instead of `{ "ok": true, ... }`.
197
-
198
- ```json
199
- {
200
- "packageRoot": "string",
201
- "homeDir": "string",
202
- "platform": "string",
203
- "nodeVersion": "string",
204
- "storageHome": "string",
205
- "storageRoot": "string",
206
- "instancesDir": "string",
207
- "instanceStorePath": "string",
208
- "legacyPackageInstancesDir": "string"
209
- }
210
- ```
211
-
212
- ### Browser commands
213
-
214
- `browser list`:
215
-
216
- ```json
217
- {
218
- "lastConnect": { "scope": "default|custom", "name": "string", "browser": "chrome|edge|chromium|brave" } | null,
219
- "instances": [{
220
- "type": "chrome|edge|chromium|brave",
221
- "name": "string",
222
- "executablePath": "string",
223
- "userDataDir": "string",
224
- "exists": true,
225
- "inUse": false,
226
- "pid": "number|null",
227
- "lockFiles": ["string"],
228
- "matchedProcess": { "pid": "number", "name": "string", "cmdline": "string" } | null,
229
- "staleLock": false,
230
- "remotePort": "number|null",
231
- "scope": "default|custom",
232
- "instanceName": "string",
233
- "createdAt": "string|null",
234
- "lastConnect": false
235
- }]
236
- }
237
- ```
238
-
239
- `browser create`:
240
-
241
- ```json
242
- {
243
- "ok": true,
244
- "instance": {
245
- "name": "string",
246
- "browser": "chrome|edge|chromium|brave",
247
- "userDataDir": "string",
248
- "createdAt": "string",
249
- "remoteDebuggingPort": "number|undefined"
250
- }
251
- }
252
- ```
253
-
254
- `browser connect`:
255
-
256
- ```json
257
- {
258
- "ok": true,
259
- "type": "chrome|edge|chromium|brave",
260
- "executablePath": "string",
261
- "userDataDir": "string",
262
- "remoteDebuggingPort": "number",
263
- "wsUrl": "string",
264
- "pid": "number|null"
265
- }
266
- ```
267
-
268
- `browser remove`:
269
-
270
- ```json
271
- {
272
- "ok": true,
273
- "removedInstance": {
274
- "name": "string",
275
- "browser": "chrome|edge|chromium|brave",
276
- "userDataDir": "string",
277
- "createdAt": "string",
278
- "remoteDebuggingPort": "number|undefined"
279
- },
280
- "removedDir": true,
281
- "closedPids": ["number"]
282
- }
283
- ```
284
-
285
- ### Session and account commands
286
-
287
- `status`:
288
-
289
- ```json
290
- {
291
- "ok": true,
292
- "instance": {
293
- "scope": "default|custom",
294
- "name": "string",
295
- "browser": "chrome|edge|chromium|brave",
296
- "source": "argument|last-connect|single-instance",
297
- "status": "running|stopped|missing|stale-lock",
298
- "exists": true,
299
- "inUse": false,
300
- "pid": "number|null",
301
- "remotePort": "number|null",
302
- "userDataDir": "string",
303
- "createdAt": "string|null",
304
- "lastConnect": false
305
- },
306
- "rednote": {
307
- "loginStatus": "logged-in|logged-out|unknown",
308
- "lastLoginAt": "string|null"
309
- }
310
- }
311
- ```
312
-
313
- `check-login`:
314
-
315
- ```json
316
- {
317
- "ok": true,
318
- "rednote": {
319
- "loginStatus": "logged-in|logged-out|unknown",
320
- "lastLoginAt": "string|null",
321
- "needLogin": false,
322
- "checkedAt": "string"
323
- }
324
- }
325
- ```
326
-
327
- `login`:
328
-
329
- ```json
330
- {
331
- "ok": true,
332
- "rednote": {
333
- "loginClicked": true,
334
- "pageUrl": "string",
335
- "waitingForPhoneLogin": true,
336
- "qrCodePath": "string|null",
337
- "message": "string"
338
- }
339
- }
340
- ```
341
-
342
- ### Feed and profile commands
343
-
344
- `home --format json`:
345
-
346
- ```json
347
- {
348
- "ok": true,
349
- "home": {
350
- "pageUrl": "string",
351
- "fetchedAt": "string",
352
- "total": "number",
353
- "posts": ["RednotePost"],
354
- "savedPath": "string|undefined"
355
- }
356
- }
357
- ```
358
-
359
- `search --format json`:
360
-
361
- ```json
362
- {
363
- "ok": true,
364
- "search": {
365
- "keyword": "string",
366
- "pageUrl": "string",
367
- "fetchedAt": "string",
368
- "total": "number",
369
- "posts": ["RednotePost"],
370
- "savedPath": "string|undefined"
371
- }
372
- }
373
- ```
374
-
375
- `get-feed-detail --format json`:
376
-
377
- ```json
378
- {
379
- "ok": true,
380
- "detail": {
381
- "fetchedAt": "string",
382
- "total": "number",
383
- "items": [{
384
- "url": "string",
385
- "note": {
386
- "noteId": "string|null",
387
- "title": "string|null",
388
- "desc": "string|null",
389
- "type": "string|null",
390
- "interactInfo": {
391
- "liked": "boolean|null",
392
- "likedCount": "string|null",
393
- "commentCount": "string|null",
394
- "collected": "boolean|null",
395
- "collectedCount": "string|null",
396
- "shareCount": "string|null",
397
- "followed": "boolean|null"
398
- },
399
- "tagList": [{ "name": "string|null" }],
400
- "imageList": [{ "urlDefault": "string|null", "urlPre": "string|null", "width": "number|null", "height": "number|null" }],
401
- "video": { "url": "string|null", "raw": "unknown" } | null,
402
- "raw": "unknown"
403
- },
404
- "comments": [{
405
- "id": "string|null",
406
- "content": "string|null",
407
- "userId": "string|null",
408
- "nickname": "string|null",
409
- "likedCount": "string|null",
410
- "subCommentCount": "number|null",
411
- "raw": "unknown"
412
- }]
413
- }]
414
- }
415
- }
416
- ```
417
-
418
- `get-profile --format json`:
419
-
420
- ```json
421
- {
422
- "ok": true,
423
- "profile": {
424
- "userId": "string",
425
- "url": "string",
426
- "fetchedAt": "string",
427
- "user": {
428
- "userId": "string|null",
429
- "nickname": "string|null",
430
- "desc": "string|null",
431
- "avatar": "string|null",
432
- "ipLocation": "string|null",
433
- "gender": "string|null",
434
- "follows": "string|number|null",
435
- "fans": "string|number|null",
436
- "interaction": "string|number|null",
437
- "tags": ["string"],
438
- "raw": "unknown"
439
- },
440
- "notes": ["RednotePost"],
441
- "raw": {
442
- "userPageData": "unknown",
443
- "notes": "unknown"
444
- }
445
- }
446
- }
447
- ```
448
-
449
- ### Action commands
450
-
451
- `publish`:
452
-
453
- ```json
454
- {
455
- "ok": true,
456
- "message": "string"
457
- }
458
- ```
459
-
460
- `interact`:
461
-
462
- ```json
463
- {
464
- "ok": true,
465
- "message": "string"
466
- }
467
- ```
468
-
469
175
  ## Storage
470
176
 
471
177
  The CLI stores browser instances and metadata under:
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ Commands:
20
20
  check-login [--instance NAME]
21
21
  login [--instance NAME]
22
22
  publish [--instance NAME]
23
- interact [--instance NAME] --url URL [--like] [--collect] [--comment TEXT]
23
+ interact [--instance NAME] [--id ID | --url URL] [--like] [--collect] [--comment TEXT]
24
24
  home [--instance NAME] [--format md|json] [--save [PATH]]
25
25
  search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
26
26
  get-feed-detail [--instance NAME] --url URL [--format md|json]
@@ -32,7 +32,7 @@ Examples:
32
32
  npx -y @skills-store/rednote browser connect --instance seller-main
33
33
  npx -y @skills-store/rednote env
34
34
  npx -y @skills-store/rednote publish --instance seller-main --type video --video ./note.mp4 --title 标题 --content 描述
35
- npx -y @skills-store/rednote interact --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect --comment "写得真好"
35
+ npx -y @skills-store/rednote interact --instance seller-main --id NOTE_ID --like --collect --comment "写得真好"
36
36
  npx -y @skills-store/rednote search --instance seller-main --keyword 护肤
37
37
  `);
38
38
  }
@@ -28,6 +28,7 @@ function renderEnvironmentMarkdown() {
28
28
  `- Package Root: ${info.packageRoot}`,
29
29
  `- Storage Home: ${info.storageHome}`,
30
30
  `- Storage Root: ${info.storageRoot}`,
31
+ `- Database: ${info.databasePath}`,
31
32
  `- Instances Dir: ${info.instancesDir}`,
32
33
  `- Instance Store: ${info.instanceStorePath}`,
33
34
  `- Legacy Package Instances: ${info.legacyPackageInstancesDir}`,
@@ -3,20 +3,23 @@ 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, simulateMousePresence, 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, resolveJsonSavePath, writeJsonFile } from './output-format.js';
10
+ import { findPersistedPostUrlByRecordId, initializeRednoteDatabase, persistFeedDetail } from './persistence.js';
9
11
  function printGetFeedDetailHelp() {
10
12
  process.stdout.write(`rednote get-feed-detail
11
13
 
12
14
  Usage:
13
- npx -y @skills-store/rednote get-feed-detail [--instance NAME] --url URL [--url URL] [--comments [COUNT]] [--format md|json] [--save PATH]
14
- node --experimental-strip-types ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--comments [COUNT]] [--format md|json] [--save PATH]
15
- bun ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--comments [COUNT]] [--format md|json] [--save PATH]
15
+ npx -y @skills-store/rednote get-feed-detail [--instance NAME] [--url URL] [--url URL] [--id ID] [--id ID] [--comments [COUNT]] [--format md|json] [--save PATH]
16
+ node --experimental-strip-types ./scripts/rednote/getFeedDetail.ts --instance NAME [--url URL] [--url URL] [--id ID] [--id ID] [--comments [COUNT]] [--format md|json] [--save PATH]
17
+ bun ./scripts/rednote/getFeedDetail.ts --instance NAME [--url URL] [--url URL] [--id ID] [--id ID] [--comments [COUNT]] [--format md|json] [--save PATH]
16
18
 
17
19
  Options:
18
20
  --instance NAME Optional. Defaults to the saved lastConnect instance
19
- --url URL Required. Xiaohongshu explore url, repeatable
21
+ --url URL Optional. Xiaohongshu explore url, repeatable
22
+ --id ID Optional. Database record id from home/search output, repeatable
20
23
  --comments [COUNT] Optional. Include comment data. When COUNT is provided, scroll \`.note-scroller\` until COUNT comments, the end, or timeout
21
24
  --format FORMAT Output format: md | json. Default: md
22
25
  --save PATH Required when --format json is used. Saves the selected result array as JSON
@@ -46,6 +49,7 @@ function parseCommentsValue(value) {
46
49
  export function parseGetFeedDetailCliArgs(argv) {
47
50
  const values = {
48
51
  urls: [],
52
+ ids: [],
49
53
  format: 'md',
50
54
  comments: undefined,
51
55
  help: false
@@ -80,6 +84,19 @@ export function parseGetFeedDetailCliArgs(argv) {
80
84
  index += 1;
81
85
  continue;
82
86
  }
87
+ if (withEquals?.key === '--id') {
88
+ values.ids.push(withEquals.value);
89
+ continue;
90
+ }
91
+ if (arg === '--id') {
92
+ const nextArg = argv[index + 1];
93
+ if (!nextArg || nextArg.startsWith('-')) {
94
+ throw new Error('Missing required option value: --id');
95
+ }
96
+ values.ids.push(nextArg);
97
+ index += 1;
98
+ continue;
99
+ }
83
100
  if (withEquals?.key === '--format') {
84
101
  values.format = withEquals.value;
85
102
  continue;
@@ -138,20 +155,6 @@ function validateFeedDetailUrl(url) {
138
155
  throw error;
139
156
  }
140
157
  }
141
- function normalizeFeedDetailUrl(url) {
142
- try {
143
- const parsed = new URL(url);
144
- if (!parsed.searchParams.has('xsec_source')) {
145
- parsed.searchParams.set('xsec_source', 'pc_feed');
146
- }
147
- return parsed.toString();
148
- } catch (error) {
149
- if (error instanceof TypeError) {
150
- throw new Error(`url is not valid: ${url}`);
151
- }
152
- throw error;
153
- }
154
- }
155
158
  async function getOrCreateXiaohongshuPage(session) {
156
159
  return session.page;
157
160
  }
@@ -179,7 +182,10 @@ async function scrollCommentsContainer(page, targetCount, getCount) {
179
182
  return;
180
183
  }
181
184
  await container.scrollIntoViewIfNeeded().catch(()=>{});
182
- await container.hover().catch(()=>{});
185
+ await simulateMouseMove(page, {
186
+ locator: container,
187
+ settleMs: 100
188
+ }).catch(()=>{});
183
189
  const getMetrics = async ()=>await container.evaluate((element)=>{
184
190
  const htmlElement = element;
185
191
  const atBottom = htmlElement.scrollTop + htmlElement.clientHeight >= htmlElement.scrollHeight - 8;
@@ -202,8 +208,12 @@ async function scrollCommentsContainer(page, targetCount, getCount) {
202
208
  }
203
209
  const beforeCount = getCount();
204
210
  const delta = Math.max(Math.floor(beforeMetrics.clientHeight * 0.85), 480);
205
- await page.mouse.wheel(0, delta).catch(()=>{});
206
- await page.waitForTimeout(900);
211
+ await simulateMouseWheel(page, {
212
+ locator: container,
213
+ deltaY: delta,
214
+ moveBeforeScroll: false,
215
+ settleMs: 900
216
+ }).catch(()=>{});
207
217
  const afterMetrics = await getMetrics();
208
218
  await page.waitForTimeout(400);
209
219
  const afterCount = getCount();
@@ -298,7 +308,7 @@ function renderDetailMarkdown(items, includeComments = false) {
298
308
  return lines.join('\n');
299
309
  }).join('\n\n---\n\n')}\n`;
300
310
  }
301
- async function captureFeedDetail(page, targetUrl, commentsOption = undefined) {
311
+ async function captureFeedDetail(page, targetUrl, commentsOption = undefined, instanceName) {
302
312
  const includeComments = hasCommentsEnabled(commentsOption);
303
313
  const commentsTarget = typeof commentsOption === 'number' ? commentsOption : null;
304
314
  let note = null;
@@ -342,6 +352,7 @@ async function captureFeedDetail(page, targetUrl, commentsOption = undefined) {
342
352
  await page.goto(targetUrl, {
343
353
  waitUntil: 'domcontentloaded'
344
354
  });
355
+ await simulateMousePresence(page);
345
356
  const deadline = Date.now() + 15_000;
346
357
  while(Date.now() < deadline){
347
358
  if (note && commentsLoaded) {
@@ -355,7 +366,7 @@ async function captureFeedDetail(page, targetUrl, commentsOption = undefined) {
355
366
  if (includeComments && commentsTarget) {
356
367
  await scrollCommentsContainer(page, commentsTarget, ()=>getCommentCount(commentsMap));
357
368
  }
358
- return {
369
+ const item = {
359
370
  url: targetUrl,
360
371
  note: normalizeDetailNote(note),
361
372
  ...includeComments ? {
@@ -364,17 +375,28 @@ async function captureFeedDetail(page, targetUrl, commentsOption = undefined) {
364
375
  ])
365
376
  } : {}
366
377
  };
378
+ if (instanceName) {
379
+ await persistFeedDetail({
380
+ instanceName,
381
+ url: targetUrl,
382
+ note: item.note,
383
+ rawNote: note,
384
+ rawComments: includeComments ? [
385
+ ...commentsMap.values()
386
+ ] : []
387
+ });
388
+ }
389
+ await simulateMousePresence(page);
390
+ return item;
367
391
  } finally{
368
392
  page.off('response', handleResponse);
369
393
  }
370
394
  }
371
- export async function getFeedDetails(session, urls, commentsOption = undefined) {
395
+ export async function getFeedDetails(session, urls, commentsOption = undefined, instanceName) {
372
396
  const page = await getOrCreateXiaohongshuPage(session);
373
397
  const items = [];
374
398
  for (const url of urls){
375
- const normalizedUrl = normalizeFeedDetailUrl(url);
376
- validateFeedDetailUrl(normalizedUrl);
377
- items.push(await captureFeedDetail(page, normalizedUrl, commentsOption));
399
+ items.push(await captureFeedDetail(page, url, commentsOption, instanceName));
378
400
  }
379
401
  return {
380
402
  ok: true,
@@ -385,6 +407,25 @@ export async function getFeedDetails(session, urls, commentsOption = undefined)
385
407
  }
386
408
  };
387
409
  }
410
+ async function resolveFeedDetailUrls(values, instanceName) {
411
+ const urls = [
412
+ ...values.urls
413
+ ];
414
+ if (values.ids.length === 0) {
415
+ return urls;
416
+ }
417
+ if (!instanceName) {
418
+ throw new Error('The --id option requires an instance-backed session.');
419
+ }
420
+ for (const id of values.ids){
421
+ const url = await findPersistedPostUrlByRecordId(instanceName, id);
422
+ if (!url) {
423
+ throw new Error(`No saved post url found for id: ${id}`);
424
+ }
425
+ urls.push(url);
426
+ }
427
+ return urls;
428
+ }
388
429
  function selectFeedDetailOutput(result) {
389
430
  return result.detail.items;
390
431
  }
@@ -400,6 +441,7 @@ function writeFeedDetailOutput(result, values) {
400
441
  }
401
442
  export async function runGetFeedDetailCommand(values = {
402
443
  urls: [],
444
+ ids: [],
403
445
  format: 'md'
404
446
  }) {
405
447
  if (values.help) {
@@ -407,14 +449,16 @@ export async function runGetFeedDetailCommand(values = {
407
449
  return;
408
450
  }
409
451
  ensureJsonSavePath(values.format, values.savePath);
410
- if (values.urls.length === 0) {
411
- throw new Error('Missing required option: --url');
452
+ if (values.urls.length === 0 && values.ids.length === 0) {
453
+ throw new Error('Missing required option: --url or --id');
412
454
  }
455
+ await initializeRednoteDatabase();
413
456
  const target = resolveStatusTarget(values.instance);
414
457
  const session = await createRednoteSession(target);
415
458
  try {
416
459
  await ensureRednoteLoggedIn(target, 'fetching feed detail', session);
417
- const result = await getFeedDetails(session, values.urls, values.comments);
460
+ const urls = await resolveFeedDetailUrls(values, target.instanceName);
461
+ const result = await getFeedDetails(session, urls, values.comments, target.instanceName);
418
462
  writeFeedDetailOutput(result, values);
419
463
  } finally{
420
464
  await disconnectRednoteSession(session);