@skills-store/rednote 0.1.7 → 0.1.9
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 +336 -2
- package/dist/index.js +12 -0
- package/dist/rednote/checkLogin.js +3 -3
- package/dist/rednote/getFeedDetail.js +64 -36
- package/dist/rednote/getProfile.js +1 -1
- package/dist/rednote/home.js +4 -4
- package/dist/rednote/index.js +7 -0
- package/dist/rednote/interact.js +304 -0
- package/dist/rednote/login.js +1 -1
- package/dist/rednote/mentions.js +0 -0
- package/dist/rednote/publish.js +1 -1
- package/dist/rednote/search.js +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @skills-store/rednote
|
|
2
2
|
|
|
3
|
-
A Xiaohongshu (RED) automation CLI for browser session management, login, search, feed detail lookup, and
|
|
3
|
+
A Xiaohongshu (RED) automation CLI for browser session management, login, search, feed detail lookup, profile lookup, and note interactions such as like, collect, and commenting through `interact`.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -27,7 +27,7 @@ For most tasks, run commands in this order:
|
|
|
27
27
|
3. browser connect
|
|
28
28
|
4. login or check-login
|
|
29
29
|
5. status
|
|
30
|
-
6. home, search, get-feed-detail,
|
|
30
|
+
6. home, search, get-feed-detail, get-profile, or interact
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
@@ -39,6 +39,7 @@ rednote browser connect --instance seller-main
|
|
|
39
39
|
rednote login --instance seller-main
|
|
40
40
|
rednote status --instance seller-main
|
|
41
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 "写得真好"
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
## Commands
|
|
@@ -121,6 +122,16 @@ rednote get-profile --instance seller-main --id USER_ID
|
|
|
121
122
|
|
|
122
123
|
Use `get-profile` when you want author or account profile information.
|
|
123
124
|
|
|
125
|
+
|
|
126
|
+
### `interact`
|
|
127
|
+
|
|
128
|
+
```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 "写得真好"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
124
135
|
## Important flags
|
|
125
136
|
|
|
126
137
|
- `--instance NAME` selects the browser instance for account-scoped commands.
|
|
@@ -130,6 +141,329 @@ Use `get-profile` when you want author or account profile information.
|
|
|
130
141
|
- `--keyword` is required for `search`.
|
|
131
142
|
- `--url` is required for `get-feed-detail`.
|
|
132
143
|
- `--id` is required for `get-profile`.
|
|
144
|
+
- `--url` is required for `interact`; at least one of `--like`, `--collect`, or `--comment TEXT` must be provided.
|
|
145
|
+
- replies are sent with `interact --comment TEXT`.
|
|
146
|
+
|
|
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
|
+
"message": "string"
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Feed and profile commands
|
|
342
|
+
|
|
343
|
+
`home --format json`:
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"ok": true,
|
|
348
|
+
"home": {
|
|
349
|
+
"pageUrl": "string",
|
|
350
|
+
"fetchedAt": "string",
|
|
351
|
+
"total": "number",
|
|
352
|
+
"posts": ["RednotePost"],
|
|
353
|
+
"savedPath": "string|undefined"
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`search --format json`:
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{
|
|
362
|
+
"ok": true,
|
|
363
|
+
"search": {
|
|
364
|
+
"keyword": "string",
|
|
365
|
+
"pageUrl": "string",
|
|
366
|
+
"fetchedAt": "string",
|
|
367
|
+
"total": "number",
|
|
368
|
+
"posts": ["RednotePost"],
|
|
369
|
+
"savedPath": "string|undefined"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`get-feed-detail --format json`:
|
|
375
|
+
|
|
376
|
+
```json
|
|
377
|
+
{
|
|
378
|
+
"ok": true,
|
|
379
|
+
"detail": {
|
|
380
|
+
"fetchedAt": "string",
|
|
381
|
+
"total": "number",
|
|
382
|
+
"items": [{
|
|
383
|
+
"url": "string",
|
|
384
|
+
"note": {
|
|
385
|
+
"noteId": "string|null",
|
|
386
|
+
"title": "string|null",
|
|
387
|
+
"desc": "string|null",
|
|
388
|
+
"type": "string|null",
|
|
389
|
+
"interactInfo": {
|
|
390
|
+
"liked": "boolean|null",
|
|
391
|
+
"likedCount": "string|null",
|
|
392
|
+
"commentCount": "string|null",
|
|
393
|
+
"collected": "boolean|null",
|
|
394
|
+
"collectedCount": "string|null",
|
|
395
|
+
"shareCount": "string|null",
|
|
396
|
+
"followed": "boolean|null"
|
|
397
|
+
},
|
|
398
|
+
"tagList": [{ "name": "string|null" }],
|
|
399
|
+
"imageList": [{ "urlDefault": "string|null", "urlPre": "string|null", "width": "number|null", "height": "number|null" }],
|
|
400
|
+
"video": { "url": "string|null", "raw": "unknown" } | null,
|
|
401
|
+
"raw": "unknown"
|
|
402
|
+
},
|
|
403
|
+
"comments": [{
|
|
404
|
+
"id": "string|null",
|
|
405
|
+
"content": "string|null",
|
|
406
|
+
"userId": "string|null",
|
|
407
|
+
"nickname": "string|null",
|
|
408
|
+
"likedCount": "string|null",
|
|
409
|
+
"subCommentCount": "number|null",
|
|
410
|
+
"raw": "unknown"
|
|
411
|
+
}]
|
|
412
|
+
}]
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
`get-profile --format json`:
|
|
418
|
+
|
|
419
|
+
```json
|
|
420
|
+
{
|
|
421
|
+
"ok": true,
|
|
422
|
+
"profile": {
|
|
423
|
+
"userId": "string",
|
|
424
|
+
"url": "string",
|
|
425
|
+
"fetchedAt": "string",
|
|
426
|
+
"user": {
|
|
427
|
+
"userId": "string|null",
|
|
428
|
+
"nickname": "string|null",
|
|
429
|
+
"desc": "string|null",
|
|
430
|
+
"avatar": "string|null",
|
|
431
|
+
"ipLocation": "string|null",
|
|
432
|
+
"gender": "string|null",
|
|
433
|
+
"follows": "string|number|null",
|
|
434
|
+
"fans": "string|number|null",
|
|
435
|
+
"interaction": "string|number|null",
|
|
436
|
+
"tags": ["string"],
|
|
437
|
+
"raw": "unknown"
|
|
438
|
+
},
|
|
439
|
+
"notes": ["RednotePost"],
|
|
440
|
+
"raw": {
|
|
441
|
+
"userPageData": "unknown",
|
|
442
|
+
"notes": "unknown"
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Action commands
|
|
449
|
+
|
|
450
|
+
`publish`:
|
|
451
|
+
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"ok": true,
|
|
455
|
+
"message": "string"
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`interact`:
|
|
460
|
+
|
|
461
|
+
```json
|
|
462
|
+
{
|
|
463
|
+
"ok": true,
|
|
464
|
+
"message": "string"
|
|
465
|
+
}
|
|
466
|
+
```
|
|
133
467
|
|
|
134
468
|
## Storage
|
|
135
469
|
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { runCli } from './utils/browser-cli.js';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { version } = require('../package.json');
|
|
3
6
|
function printRootHelp() {
|
|
4
7
|
process.stdout.write(`rednote
|
|
8
|
+
Version:
|
|
9
|
+
${version}
|
|
5
10
|
|
|
6
11
|
Usage:
|
|
7
12
|
npx -y @skills-store/rednote browser <command> [...args]
|
|
8
13
|
npx -y @skills-store/rednote <command> [...args]
|
|
9
14
|
|
|
10
15
|
Commands:
|
|
16
|
+
--version
|
|
11
17
|
browser <list|create|remove|connect>
|
|
12
18
|
env [--format md|json]
|
|
13
19
|
status [--instance NAME]
|
|
14
20
|
check-login [--instance NAME]
|
|
15
21
|
login [--instance NAME]
|
|
16
22
|
publish [--instance NAME]
|
|
23
|
+
interact [--instance NAME] --url URL [--like] [--collect] [--comment TEXT]
|
|
17
24
|
home [--instance NAME] [--format md|json] [--save [PATH]]
|
|
18
25
|
search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
|
|
19
26
|
get-feed-detail [--instance NAME] --url URL [--format md|json]
|
|
@@ -25,6 +32,7 @@ Examples:
|
|
|
25
32
|
npx -y @skills-store/rednote browser connect --instance seller-main
|
|
26
33
|
npx -y @skills-store/rednote env
|
|
27
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 "写得真好"
|
|
28
36
|
npx -y @skills-store/rednote search --instance seller-main --keyword 护肤
|
|
29
37
|
`);
|
|
30
38
|
}
|
|
@@ -34,6 +42,10 @@ export async function runRootCli(argv = process.argv.slice(2)) {
|
|
|
34
42
|
printRootHelp();
|
|
35
43
|
return;
|
|
36
44
|
}
|
|
45
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
46
|
+
process.stdout.write(`${version}\n`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
37
49
|
if (command === 'browser') {
|
|
38
50
|
const { runBrowserCli } = await import('./browser/index.js');
|
|
39
51
|
await runBrowserCli(restArgv);
|
|
@@ -55,9 +55,9 @@ export async function createRednoteSession(target) {
|
|
|
55
55
|
page
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
export function disconnectRednoteSession(session) {
|
|
58
|
+
export async function disconnectRednoteSession(session) {
|
|
59
59
|
try {
|
|
60
|
-
session.browser.
|
|
60
|
+
await session.browser.close();
|
|
61
61
|
} catch {}
|
|
62
62
|
}
|
|
63
63
|
export async function checkRednoteLogin(target, session) {
|
|
@@ -91,7 +91,7 @@ export async function checkRednoteLogin(target, session) {
|
|
|
91
91
|
};
|
|
92
92
|
} finally{
|
|
93
93
|
if (ownsSession) {
|
|
94
|
-
disconnectRednoteSession(activeSession);
|
|
94
|
+
await disconnectRednoteSession(activeSession);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -72,6 +72,20 @@ function validateFeedDetailUrl(url) {
|
|
|
72
72
|
throw error;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
function normalizeFeedDetailUrl(url) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = new URL(url);
|
|
78
|
+
if (!parsed.searchParams.has('xsec_source')) {
|
|
79
|
+
parsed.searchParams.set('xsec_source', 'pc_feed');
|
|
80
|
+
}
|
|
81
|
+
return parsed.toString();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof TypeError) {
|
|
84
|
+
throw new Error(`url is not valid: ${url}`);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
75
89
|
async function getOrCreateXiaohongshuPage(session) {
|
|
76
90
|
return session.page;
|
|
77
91
|
}
|
|
@@ -87,10 +101,13 @@ function normalizeDetailNote(note) {
|
|
|
87
101
|
desc: note?.desc ?? null,
|
|
88
102
|
type: note?.type ?? null,
|
|
89
103
|
interactInfo: {
|
|
104
|
+
liked: note?.interactInfo?.liked ?? null,
|
|
90
105
|
likedCount: note?.interactInfo?.likedCount ?? null,
|
|
91
106
|
commentCount: note?.interactInfo?.commentCount ?? null,
|
|
107
|
+
collected: note?.interactInfo?.collected ?? null,
|
|
92
108
|
collectedCount: note?.interactInfo?.collectedCount ?? null,
|
|
93
|
-
shareCount: note?.interactInfo?.shareCount ?? null
|
|
109
|
+
shareCount: note?.interactInfo?.shareCount ?? null,
|
|
110
|
+
followed: note?.interactInfo?.followed ?? null
|
|
94
111
|
},
|
|
95
112
|
tagList: Array.isArray(note?.tagList) ? note.tagList.map((tag)=>({
|
|
96
113
|
name: tag?.name ?? null
|
|
@@ -119,45 +136,55 @@ function normalizeComments(comments) {
|
|
|
119
136
|
raw: comment
|
|
120
137
|
}));
|
|
121
138
|
}
|
|
139
|
+
function formatDetailField(value) {
|
|
140
|
+
return value ?? '';
|
|
141
|
+
}
|
|
122
142
|
function renderDetailMarkdown(items) {
|
|
123
143
|
if (items.length === 0) {
|
|
124
144
|
return '没有获取到帖子详情。\n';
|
|
125
145
|
}
|
|
126
146
|
return `${items.map((item)=>{
|
|
127
|
-
const lines = [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
lines.push(
|
|
131
|
-
lines.push(
|
|
132
|
-
lines.push(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
lines.push(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
lines.push('### 评论:');
|
|
156
|
-
for (const comment of item.comments){
|
|
157
|
-
lines.push(`- ${comment.content ?? ''}`);
|
|
147
|
+
const lines = [];
|
|
148
|
+
lines.push('## Note');
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push(`- Url: ${item.url}`);
|
|
151
|
+
lines.push(`- Title: ${formatDetailField(item.note.title)}`);
|
|
152
|
+
lines.push(`- Type: ${formatDetailField(item.note.type)}`);
|
|
153
|
+
lines.push(`- Liked: ${formatDetailField(item.note.interactInfo.liked)}`);
|
|
154
|
+
lines.push(`- Collected: ${formatDetailField(item.note.interactInfo.collected)}`);
|
|
155
|
+
lines.push(`- LikedCount: ${formatDetailField(item.note.interactInfo.likedCount)}`);
|
|
156
|
+
lines.push(`- CommentCount: ${formatDetailField(item.note.interactInfo.commentCount)}`);
|
|
157
|
+
lines.push(`- CollectedCount: ${formatDetailField(item.note.interactInfo.collectedCount)}`);
|
|
158
|
+
lines.push(`- ShareCount: ${formatDetailField(item.note.interactInfo.shareCount)}`);
|
|
159
|
+
lines.push(`- Tags: ${item.note.tagList.map((tag)=>tag.name ? `#${tag.name}` : '').filter(Boolean).join(' ')}`);
|
|
160
|
+
lines.push('');
|
|
161
|
+
lines.push('## Content');
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push(item.note.desc ?? '');
|
|
164
|
+
if (item.note.imageList.length > 0 || item.note.video?.url) {
|
|
165
|
+
lines.push('');
|
|
166
|
+
lines.push('## Media');
|
|
167
|
+
lines.push('');
|
|
168
|
+
item.note.imageList.forEach((image, index)=>{
|
|
169
|
+
if (image.urlDefault) {
|
|
170
|
+
lines.push(`- Image${index + 1}: ${image.urlDefault}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
if (item.note.video?.url) {
|
|
174
|
+
lines.push(`- Video: ${item.note.video.url}`);
|
|
158
175
|
}
|
|
159
176
|
}
|
|
160
|
-
lines.push('
|
|
177
|
+
lines.push('');
|
|
178
|
+
lines.push('## Comments');
|
|
179
|
+
lines.push('');
|
|
180
|
+
if (item.comments.length === 0) {
|
|
181
|
+
lines.push('- Comments not found');
|
|
182
|
+
} else {
|
|
183
|
+
item.comments.forEach((comment)=>{
|
|
184
|
+
const prefix = comment.nickname ? `${comment.nickname}: ` : '';
|
|
185
|
+
lines.push(`- ${prefix}${comment.content ?? ''}`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
161
188
|
return lines.join('\n');
|
|
162
189
|
}).join('\n\n---\n\n')}\n`;
|
|
163
190
|
}
|
|
@@ -221,8 +248,9 @@ export async function getFeedDetails(session, urls) {
|
|
|
221
248
|
const page = await getOrCreateXiaohongshuPage(session);
|
|
222
249
|
const items = [];
|
|
223
250
|
for (const url of urls){
|
|
224
|
-
|
|
225
|
-
|
|
251
|
+
const normalizedUrl = normalizeFeedDetailUrl(url);
|
|
252
|
+
validateFeedDetailUrl(normalizedUrl);
|
|
253
|
+
items.push(await captureFeedDetail(page, normalizedUrl));
|
|
226
254
|
}
|
|
227
255
|
return {
|
|
228
256
|
ok: true,
|
|
@@ -258,7 +286,7 @@ export async function runGetFeedDetailCommand(values = {
|
|
|
258
286
|
const result = await getFeedDetails(session, values.urls);
|
|
259
287
|
writeFeedDetailOutput(result, values.format);
|
|
260
288
|
} finally{
|
|
261
|
-
disconnectRednoteSession(session);
|
|
289
|
+
await disconnectRednoteSession(session);
|
|
262
290
|
}
|
|
263
291
|
}
|
|
264
292
|
async function main() {
|
|
@@ -317,7 +317,7 @@ export async function runGetProfileCommand(values = {
|
|
|
317
317
|
const result = await getProfile(session, buildProfileUrl(normalizedUserId), normalizedUserId);
|
|
318
318
|
writeProfileOutput(result, values.format);
|
|
319
319
|
} finally{
|
|
320
|
-
disconnectRednoteSession(session);
|
|
320
|
+
await disconnectRednoteSession(session);
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
async function main() {
|
package/dist/rednote/home.js
CHANGED
|
@@ -99,7 +99,7 @@ async function collectHomeFeedItems(page) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
const url = new URL(response.url());
|
|
102
|
-
if (
|
|
102
|
+
if (url.pathname !== '/explore' && url.pathname !== '/explore/') {
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
const html = await response.text();
|
|
@@ -138,12 +138,12 @@ async function collectHomeFeedItems(page) {
|
|
|
138
138
|
}, 15_000);
|
|
139
139
|
page.on('response', handleResponse);
|
|
140
140
|
});
|
|
141
|
-
if (page.url()
|
|
141
|
+
if (page.url() === 'https://www.xiaohongshu.com/explore/') {
|
|
142
142
|
await page.reload({
|
|
143
143
|
waitUntil: 'domcontentloaded'
|
|
144
144
|
});
|
|
145
145
|
} else {
|
|
146
|
-
await page.goto('https://www.xiaohongshu.com/explore', {
|
|
146
|
+
await page.goto('https://www.xiaohongshu.com/explore/', {
|
|
147
147
|
waitUntil: 'domcontentloaded'
|
|
148
148
|
});
|
|
149
149
|
}
|
|
@@ -196,7 +196,7 @@ export async function runHomeCommand(values = {
|
|
|
196
196
|
const result = await getRednoteHomePosts(session);
|
|
197
197
|
writeHomeOutput(result, values);
|
|
198
198
|
} finally{
|
|
199
|
-
disconnectRednoteSession(session);
|
|
199
|
+
await disconnectRednoteSession(session);
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
async function main() {
|
package/dist/rednote/index.js
CHANGED
|
@@ -11,6 +11,7 @@ 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
15
|
home [--instance NAME] [--format md|json] [--save [PATH]]
|
|
15
16
|
search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
|
|
16
17
|
get-feed-detail [--instance NAME] --url URL [--url URL] [--format md|json]
|
|
@@ -23,6 +24,7 @@ Examples:
|
|
|
23
24
|
npx -y @skills-store/rednote status --instance seller-main
|
|
24
25
|
npx -y @skills-store/rednote login --instance seller-main
|
|
25
26
|
npx -y @skills-store/rednote publish --instance seller-main --type video --video ./note.mp4 --title 标题 --content 描述
|
|
27
|
+
npx -y @skills-store/rednote interact --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect --comment "写得真好"
|
|
26
28
|
npx -y @skills-store/rednote home --instance seller-main --format md --save
|
|
27
29
|
npx -y @skills-store/rednote search --instance seller-main --keyword 护肤 --format json --save ./output/search.jsonl
|
|
28
30
|
npx -y @skills-store/rednote get-feed-detail --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy"
|
|
@@ -99,6 +101,11 @@ export async function runRednoteCli(argv = process.argv.slice(2)) {
|
|
|
99
101
|
await runPublishCommand(parsePublishCliArgs(commandArgv));
|
|
100
102
|
return;
|
|
101
103
|
}
|
|
104
|
+
if (command === 'interact') {
|
|
105
|
+
const { parseInteractCliArgs, runInteractCommand } = await import('./interact.js');
|
|
106
|
+
await runInteractCommand(parseInteractCliArgs(commandArgv));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
102
109
|
if (command === 'home') {
|
|
103
110
|
const { parseHomeCliArgs, runHomeCommand } = await import('./home.js');
|
|
104
111
|
await runHomeCommand(parseHomeCliArgs(commandArgv));
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { printJson, runCli } from '../utils/browser-cli.js';
|
|
4
|
+
import { resolveStatusTarget } from './status.js';
|
|
5
|
+
import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
|
|
6
|
+
import { getFeedDetails } from './getFeedDetail.js';
|
|
7
|
+
const INTERACT_CONTAINER_SELECTOR = '.interact-container';
|
|
8
|
+
const LIKE_WRAPPER_SELECTOR = `${INTERACT_CONTAINER_SELECTOR} .like-wrapper`;
|
|
9
|
+
const COLLECT_WRAPPER_SELECTOR = `${INTERACT_CONTAINER_SELECTOR} .collect-wrapper, ${INTERACT_CONTAINER_SELECTOR} #note-page-collect-board-guide`;
|
|
10
|
+
const COMMENT_INPUT_SELECTOR = '#content-textarea[contenteditable="true"]';
|
|
11
|
+
const COMMENT_SEND_BUTTON_SELECTOR = 'button.btn.submit';
|
|
12
|
+
const COMMENT_SEND_BUTTON_TEXT = '发送';
|
|
13
|
+
function printInteractHelp() {
|
|
14
|
+
process.stdout.write(`rednote interact
|
|
15
|
+
|
|
16
|
+
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]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--instance NAME Optional. Defaults to the saved lastConnect instance
|
|
23
|
+
--url URL Required. Xiaohongshu explore url
|
|
24
|
+
--like Optional. Perform like
|
|
25
|
+
--collect Optional. Perform collect
|
|
26
|
+
--comment TEXT Optional. Post comment content
|
|
27
|
+
-h, --help Show this help
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
export function parseInteractCliArgs(argv) {
|
|
31
|
+
const { values, positionals } = parseArgs({
|
|
32
|
+
args: argv,
|
|
33
|
+
allowPositionals: true,
|
|
34
|
+
strict: false,
|
|
35
|
+
options: {
|
|
36
|
+
instance: {
|
|
37
|
+
type: 'string'
|
|
38
|
+
},
|
|
39
|
+
url: {
|
|
40
|
+
type: 'string'
|
|
41
|
+
},
|
|
42
|
+
like: {
|
|
43
|
+
type: 'boolean'
|
|
44
|
+
},
|
|
45
|
+
collect: {
|
|
46
|
+
type: 'boolean'
|
|
47
|
+
},
|
|
48
|
+
comment: {
|
|
49
|
+
type: 'string'
|
|
50
|
+
},
|
|
51
|
+
help: {
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
short: 'h'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (positionals.length > 0) {
|
|
58
|
+
throw new Error(`Unexpected positional arguments: ${positionals.join(' ')}`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
instance: values.instance,
|
|
62
|
+
url: values.url,
|
|
63
|
+
like: values.like,
|
|
64
|
+
collect: values.collect,
|
|
65
|
+
comment: values.comment,
|
|
66
|
+
help: values.help
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function ensureNonEmpty(value, optionName) {
|
|
70
|
+
const normalized = value?.trim();
|
|
71
|
+
if (!normalized) {
|
|
72
|
+
throw new Error(`Missing required option: ${optionName}`);
|
|
73
|
+
}
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
function validateFeedDetailUrl(url) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = new URL(url);
|
|
79
|
+
if (!parsed.href.startsWith('https://www.xiaohongshu.com/explore/')) {
|
|
80
|
+
throw new Error(`url is not valid: ${url},must start with "https://www.xiaohongshu.com/explore/"`);
|
|
81
|
+
}
|
|
82
|
+
if (!parsed.searchParams.get('xsec_token')) {
|
|
83
|
+
throw new Error(`url is not valid: ${url},must include "xsec_token="`);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof TypeError) {
|
|
87
|
+
throw new Error(`url is not valid: ${url}`);
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function resolveInteractActions(values) {
|
|
93
|
+
const actions = [];
|
|
94
|
+
if (values.like) {
|
|
95
|
+
actions.push('like');
|
|
96
|
+
}
|
|
97
|
+
if (values.collect) {
|
|
98
|
+
actions.push('collect');
|
|
99
|
+
}
|
|
100
|
+
const commentContent = values.comment?.trim();
|
|
101
|
+
if (commentContent) {
|
|
102
|
+
actions.push('comment');
|
|
103
|
+
}
|
|
104
|
+
if (actions.length === 0) {
|
|
105
|
+
throw new Error('At least one interact option is required: --like, --collect, or --comment');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
actions,
|
|
109
|
+
commentContent
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function getOrCreateXiaohongshuPage(session) {
|
|
113
|
+
return session.page;
|
|
114
|
+
}
|
|
115
|
+
async function findVisibleLocator(locator, timeoutMs = 5_000) {
|
|
116
|
+
const deadline = Date.now() + timeoutMs;
|
|
117
|
+
while(Date.now() < deadline){
|
|
118
|
+
const count = await locator.count();
|
|
119
|
+
for(let index = 0; index < count; index += 1){
|
|
120
|
+
const candidate = locator.nth(index);
|
|
121
|
+
if (await candidate.isVisible().catch(()=>false)) {
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
async function requireVisibleLocator(locator, errorMessage, timeoutMs = 5_000) {
|
|
130
|
+
const visibleLocator = await findVisibleLocator(locator, timeoutMs);
|
|
131
|
+
if (!visibleLocator) {
|
|
132
|
+
throw new Error(errorMessage);
|
|
133
|
+
}
|
|
134
|
+
return visibleLocator;
|
|
135
|
+
}
|
|
136
|
+
async function typeCommentContent(page, content) {
|
|
137
|
+
const commentInput = page.locator(COMMENT_INPUT_SELECTOR);
|
|
138
|
+
const visibleCommentInput = await requireVisibleLocator(commentInput, '未找到评论输入框,请确认帖子详情页已正确加载。', 15_000);
|
|
139
|
+
await visibleCommentInput.scrollIntoViewIfNeeded();
|
|
140
|
+
await visibleCommentInput.click({
|
|
141
|
+
force: true
|
|
142
|
+
});
|
|
143
|
+
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A').catch(()=>{});
|
|
144
|
+
await page.keyboard.press('Backspace').catch(()=>{});
|
|
145
|
+
await page.keyboard.insertText(content);
|
|
146
|
+
await page.waitForFunction(({ selector, expectedContent })=>{
|
|
147
|
+
const element = document.querySelector(selector);
|
|
148
|
+
if (!(element instanceof HTMLElement)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return element.innerText.trim() === expectedContent;
|
|
152
|
+
}, {
|
|
153
|
+
selector: COMMENT_INPUT_SELECTOR,
|
|
154
|
+
expectedContent: content
|
|
155
|
+
}, {
|
|
156
|
+
timeout: 5_000
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
async function clickSendComment(page) {
|
|
160
|
+
const sendButton = page.locator(COMMENT_SEND_BUTTON_SELECTOR).filter({
|
|
161
|
+
hasText: COMMENT_SEND_BUTTON_TEXT
|
|
162
|
+
});
|
|
163
|
+
const visibleSendButton = await requireVisibleLocator(sendButton, '未找到“发送”按钮,请确认评论工具栏已正确加载。', 15_000);
|
|
164
|
+
await page.waitForFunction(({ selector, text })=>{
|
|
165
|
+
const buttons = [
|
|
166
|
+
...document.querySelectorAll(selector)
|
|
167
|
+
];
|
|
168
|
+
const target = buttons.find((candidate)=>candidate.textContent?.includes(text));
|
|
169
|
+
return target instanceof HTMLButtonElement && !target.disabled;
|
|
170
|
+
}, {
|
|
171
|
+
selector: COMMENT_SEND_BUTTON_SELECTOR,
|
|
172
|
+
text: COMMENT_SEND_BUTTON_TEXT
|
|
173
|
+
}, {
|
|
174
|
+
timeout: 5_000
|
|
175
|
+
});
|
|
176
|
+
await visibleSendButton.click();
|
|
177
|
+
await page.waitForFunction(({ inputSelector, buttonSelector, text })=>{
|
|
178
|
+
const input = document.querySelector(inputSelector);
|
|
179
|
+
const buttons = [
|
|
180
|
+
...document.querySelectorAll(buttonSelector)
|
|
181
|
+
];
|
|
182
|
+
const button = buttons.find((candidate)=>candidate.textContent?.includes(text));
|
|
183
|
+
const inputCleared = input instanceof HTMLElement ? input.innerText.trim().length === 0 : false;
|
|
184
|
+
const buttonDisabled = button instanceof HTMLButtonElement ? button.disabled : false;
|
|
185
|
+
return inputCleared || buttonDisabled;
|
|
186
|
+
}, {
|
|
187
|
+
inputSelector: COMMENT_INPUT_SELECTOR,
|
|
188
|
+
buttonSelector: COMMENT_SEND_BUTTON_SELECTOR,
|
|
189
|
+
text: COMMENT_SEND_BUTTON_TEXT
|
|
190
|
+
}, {
|
|
191
|
+
timeout: 10_000
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async function commentOnCurrentFeedPage(page, content) {
|
|
195
|
+
await typeCommentContent(page, content);
|
|
196
|
+
await clickSendComment(page);
|
|
197
|
+
}
|
|
198
|
+
async function waitForInteractContainer(page) {
|
|
199
|
+
await page.waitForLoadState('domcontentloaded');
|
|
200
|
+
await page.waitForTimeout(500);
|
|
201
|
+
await requireVisibleLocator(page.locator(INTERACT_CONTAINER_SELECTOR), '未找到互动工具栏,请确认帖子详情页已正确加载。', 15_000);
|
|
202
|
+
}
|
|
203
|
+
function getActionErrorMessage(action) {
|
|
204
|
+
return action === 'like' ? '未找到点赞按钮,请确认帖子详情页已正确加载。' : '未找到收藏按钮,请确认帖子详情页已正确加载。';
|
|
205
|
+
}
|
|
206
|
+
async function ensureActionApplied(page, action, alreadyActive) {
|
|
207
|
+
if (alreadyActive) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
const locator = page.locator(action === 'like' ? LIKE_WRAPPER_SELECTOR : COLLECT_WRAPPER_SELECTOR);
|
|
211
|
+
const visibleLocator = await requireVisibleLocator(locator, getActionErrorMessage(action), 15_000);
|
|
212
|
+
await visibleLocator.scrollIntoViewIfNeeded();
|
|
213
|
+
await visibleLocator.click({
|
|
214
|
+
force: true
|
|
215
|
+
});
|
|
216
|
+
await page.waitForFunction(({ selector, currentAction })=>{
|
|
217
|
+
const nodes = [
|
|
218
|
+
...document.querySelectorAll(selector)
|
|
219
|
+
];
|
|
220
|
+
const target = nodes.find((candidate)=>candidate instanceof HTMLElement && candidate.offsetParent !== null);
|
|
221
|
+
if (!(target instanceof HTMLElement)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const classNames = [
|
|
225
|
+
...target.classList
|
|
226
|
+
].map((item)=>item.toLowerCase());
|
|
227
|
+
if (classNames.includes(`${currentAction}-active`) || classNames.includes('active') || classNames.some((item)=>item.endsWith('-active'))) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
const iconRefs = [
|
|
231
|
+
...target.querySelectorAll('use')
|
|
232
|
+
].map((node)=>(node.getAttribute('xlink:href') ?? node.getAttribute('href') ?? '').toLowerCase()).filter(Boolean).join(' ');
|
|
233
|
+
if (currentAction === 'like') {
|
|
234
|
+
return iconRefs.includes('liked') || iconRefs.includes('like-filled') || iconRefs.includes('like_fill');
|
|
235
|
+
}
|
|
236
|
+
return iconRefs.includes('collected') || iconRefs.includes('collect-filled') || iconRefs.includes('collect_fill');
|
|
237
|
+
}, {
|
|
238
|
+
selector: action === 'like' ? LIKE_WRAPPER_SELECTOR : COLLECT_WRAPPER_SELECTOR,
|
|
239
|
+
currentAction: action
|
|
240
|
+
}, {
|
|
241
|
+
timeout: 10_000
|
|
242
|
+
});
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
export async function interactWithFeed(session, url, actions, commentContent) {
|
|
246
|
+
validateFeedDetailUrl(url);
|
|
247
|
+
const detailResult = await getFeedDetails(session, [
|
|
248
|
+
url
|
|
249
|
+
]);
|
|
250
|
+
const detailItem = detailResult.detail.items[0];
|
|
251
|
+
if (!detailItem) {
|
|
252
|
+
throw new Error(`Failed to load feed detail: ${url}`);
|
|
253
|
+
}
|
|
254
|
+
const page = await getOrCreateXiaohongshuPage(session);
|
|
255
|
+
await waitForInteractContainer(page);
|
|
256
|
+
let liked = detailItem.note.interactInfo.liked === true;
|
|
257
|
+
let collected = detailItem.note.interactInfo.collected === true;
|
|
258
|
+
const messages = [];
|
|
259
|
+
for (const action of actions){
|
|
260
|
+
if (action === 'like') {
|
|
261
|
+
const alreadyActive = liked;
|
|
262
|
+
await ensureActionApplied(page, 'like', alreadyActive);
|
|
263
|
+
liked = true;
|
|
264
|
+
messages.push(alreadyActive ? 'Like already active' : 'Like completed');
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (action === 'collect') {
|
|
268
|
+
const alreadyActive = collected;
|
|
269
|
+
await ensureActionApplied(page, 'collect', alreadyActive);
|
|
270
|
+
collected = true;
|
|
271
|
+
messages.push(alreadyActive ? 'Collect already active' : 'Collect completed');
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const normalizedContent = ensureNonEmpty(commentContent, '--comment');
|
|
275
|
+
await commentOnCurrentFeedPage(page, normalizedContent);
|
|
276
|
+
messages.push('Comment posted');
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
message: `${messages.join('; ')}: ${url}`
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export async function runInteractCommand(values = {}) {
|
|
284
|
+
if (values.help) {
|
|
285
|
+
printInteractHelp();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const url = ensureNonEmpty(values.url, '--url');
|
|
289
|
+
const { actions, commentContent } = resolveInteractActions(values);
|
|
290
|
+
const target = resolveStatusTarget(values.instance);
|
|
291
|
+
const session = await createRednoteSession(target);
|
|
292
|
+
try {
|
|
293
|
+
await ensureRednoteLoggedIn(target, `performing ${actions.join(', ')} interact`, session);
|
|
294
|
+
const result = await interactWithFeed(session, url, actions, commentContent);
|
|
295
|
+
printJson(result);
|
|
296
|
+
} finally{
|
|
297
|
+
await disconnectRednoteSession(session);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function main() {
|
|
301
|
+
const values = parseInteractCliArgs(process.argv.slice(2));
|
|
302
|
+
await runInteractCommand(values);
|
|
303
|
+
}
|
|
304
|
+
runCli(import.meta.url, main);
|
package/dist/rednote/login.js
CHANGED
|
@@ -79,7 +79,7 @@ export async function runLoginCommand(values = {}) {
|
|
|
79
79
|
const result = await openRednoteLogin(target, session);
|
|
80
80
|
printJson(result);
|
|
81
81
|
} finally{
|
|
82
|
-
disconnectRednoteSession(session);
|
|
82
|
+
await disconnectRednoteSession(session);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
async function main() {
|
|
File without changes
|
package/dist/rednote/publish.js
CHANGED
|
@@ -483,7 +483,7 @@ export async function runPublishCommand(values) {
|
|
|
483
483
|
const result = await openRednotePublish(session, payload);
|
|
484
484
|
printJson(result);
|
|
485
485
|
} finally{
|
|
486
|
-
disconnectRednoteSession(session);
|
|
486
|
+
await disconnectRednoteSession(session);
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
async function main() {
|
package/dist/rednote/search.js
CHANGED
|
@@ -193,7 +193,7 @@ export async function runSearchCommand(values = {
|
|
|
193
193
|
const result = await searchRednotePosts(session, keyword);
|
|
194
194
|
writeSearchOutput(result, values);
|
|
195
195
|
} finally{
|
|
196
|
-
disconnectRednoteSession(session);
|
|
196
|
+
await disconnectRednoteSession(session);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skills-store/rednote",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"check-login": "node --experimental-strip-types ./scripts/rednote/checkLogin.ts",
|
|
17
17
|
"login": "node --experimental-strip-types ./scripts/rednote/login.ts",
|
|
18
18
|
"publish": "node --experimental-strip-types ./scripts/rednote/publish.ts",
|
|
19
|
+
"interact": "node --experimental-strip-types ./scripts/rednote/interact.ts",
|
|
19
20
|
"home": "node --experimental-strip-types ./scripts/rednote/home.ts",
|
|
20
21
|
"search": "node --experimental-strip-types ./scripts/rednote/search.ts",
|
|
21
22
|
"get-feed-detail": "node --experimental-strip-types ./scripts/rednote/getFeedDetail.ts",
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
"bun:check-login": "bun ./scripts/rednote/checkLogin.ts",
|
|
28
29
|
"bun:login": "bun ./scripts/rednote/login.ts",
|
|
29
30
|
"bun:publish": "bun ./scripts/rednote/publish.ts",
|
|
31
|
+
"bun:interact": "bun ./scripts/rednote/interact.ts",
|
|
30
32
|
"bun:home": "bun ./scripts/rednote/home.ts",
|
|
31
33
|
"bun:search": "bun ./scripts/rednote/search.ts",
|
|
32
34
|
"bun:get-feed-detail": "bun ./scripts/rednote/getFeedDetail.ts",
|