@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.
- package/README.md +19 -327
- package/dist/browser/connect-browser.js +59 -31
- package/dist/index.js +2 -2
- package/dist/rednote/checkLogin.js +6 -0
- package/dist/rednote/getMyProfile.js +139 -0
- package/dist/rednote/getProfile.js +145 -31
- package/dist/rednote/index.js +8 -2
- package/dist/rednote/interact.js +30 -7
- package/dist/rednote/login.js +15 -21
- package/dist/rednote/persistence.js +187 -1
- package/dist/rednote/status.js +4 -0
- package/dist/utils/browser-cli.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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 ||
|
|
376
|
+
if (userPageData || notesMap.size > 0) {
|
|
273
377
|
break;
|
|
274
378
|
}
|
|
275
379
|
await page.waitForTimeout(200);
|
|
276
380
|
}
|
|
277
|
-
if (!userPageData &&
|
|
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);
|
package/dist/rednote/index.js
CHANGED
|
@@ -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 --
|
|
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() {
|
package/dist/rednote/interact.js
CHANGED
|
@@ -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
|
-
--
|
|
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.
|
|
257
|
-
let collected = detailItem.note.
|
|
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{
|
package/dist/rednote/login.js
CHANGED
|
@@ -103,13 +103,11 @@ export async function openRednoteLogin(target, session) {
|
|
|
103
103
|
if (!rednoteStatus.needLogin) {
|
|
104
104
|
return {
|
|
105
105
|
ok: true,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 = {}) {
|