@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 +45 -339
- package/dist/index.js +2 -2
- package/dist/rednote/env.js +1 -0
- package/dist/rednote/getFeedDetail.js +74 -30
- package/dist/rednote/getProfile.js +3 -2
- package/dist/rednote/home.js +39 -14
- package/dist/rednote/index.js +2 -2
- package/dist/rednote/interact.js +30 -7
- package/dist/rednote/output-format.js +10 -0
- package/dist/rednote/persistence.js +578 -0
- package/dist/rednote/search.js +41 -14
- package/dist/rednote/url-format.js +41 -0
- package/dist/utils/browser-core.js +2 -0
- package/dist/utils/mouse-helper.js +105 -0
- package/package.json +7 -2
|
@@ -121,6 +121,7 @@ function normalizeProfileUser(userPageData) {
|
|
|
121
121
|
tags
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
|
+
import { buildExploreUrl, decodeUrlEscapedValue } from './url-format.js';
|
|
124
125
|
function normalizeProfileNote(item) {
|
|
125
126
|
const id = firstNonNull(item.id, item.noteId);
|
|
126
127
|
if (!id) {
|
|
@@ -132,12 +133,12 @@ function normalizeProfileNote(item) {
|
|
|
132
133
|
const cover = noteCard.cover ?? {};
|
|
133
134
|
const imageList = Array.isArray(noteCard.imageList ?? noteCard.image_list) ? noteCard.imageList ?? noteCard.image_list : [];
|
|
134
135
|
const cornerTagInfo = Array.isArray(noteCard.cornerTagInfo ?? noteCard.corner_tag_info) ? noteCard.cornerTagInfo ?? noteCard.corner_tag_info : [];
|
|
135
|
-
const xsecToken = firstNonNull(item.xsecToken, item.xsec_token);
|
|
136
|
+
const xsecToken = decodeUrlEscapedValue(firstNonNull(item.xsecToken, item.xsec_token));
|
|
136
137
|
return {
|
|
137
138
|
id,
|
|
138
139
|
modelType: firstNonNull(item.modelType, item.model_type) ?? 'note',
|
|
139
140
|
xsecToken,
|
|
140
|
-
url:
|
|
141
|
+
url: buildExploreUrl(id, xsecToken),
|
|
141
142
|
noteCard: {
|
|
142
143
|
type: firstNonNull(noteCard.type, null),
|
|
143
144
|
displayTitle: firstNonNull(noteCard.displayTitle, noteCard.display_title),
|
package/dist/rednote/home.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { runCli } from '../utils/browser-cli.js';
|
|
4
|
+
import { simulateMousePresence } from '../utils/mouse-helper.js';
|
|
4
5
|
import { resolveStatusTarget } from './status.js';
|
|
5
6
|
import * as cheerio from 'cheerio';
|
|
6
7
|
import vm from 'node:vm';
|
|
7
|
-
import { ensureJsonSavePath, parseOutputCliArgs,
|
|
8
|
+
import { ensureJsonSavePath, parseOutputCliArgs, renderPostSummaryList, resolveJsonSavePath, resolveSavePath, writeJsonFile, writePostsJsonl } from './output-format.js';
|
|
8
9
|
import { createRednoteSession, disconnectRednoteSession } from './checkLogin.js';
|
|
10
|
+
import { initializeRednoteDatabase, listPersistedPostSummaries, persistHomePosts } from './persistence.js';
|
|
9
11
|
export function parseHomeCliArgs(argv) {
|
|
10
12
|
return parseOutputCliArgs(argv);
|
|
11
13
|
}
|
|
@@ -32,11 +34,12 @@ function normalizeHomePost(item) {
|
|
|
32
34
|
const imageList = Array.isArray(noteCard.imageList) ? noteCard.imageList : [];
|
|
33
35
|
const cornerTagInfo = Array.isArray(noteCard.cornerTagInfo) ? noteCard.cornerTagInfo : [];
|
|
34
36
|
const xsecToken = item.xsecToken ?? null;
|
|
37
|
+
const url = xsecToken ? `https://www.xiaohongshu.com/explore/${item.id}?xsec_token=${xsecToken}` : `https://www.xiaohongshu.com/explore/${item.id}`;
|
|
35
38
|
return {
|
|
36
39
|
id: item.id,
|
|
37
40
|
modelType: item.modelType,
|
|
38
41
|
xsecToken,
|
|
39
|
-
url
|
|
42
|
+
url,
|
|
40
43
|
noteCard: {
|
|
41
44
|
type: noteCard.type ?? null,
|
|
42
45
|
displayTitle: noteCard.displayTitle ?? null,
|
|
@@ -84,6 +87,20 @@ function normalizeHomePost(item) {
|
|
|
84
87
|
}
|
|
85
88
|
};
|
|
86
89
|
}
|
|
90
|
+
function buildPostSummaryList(posts, persistedRows = []) {
|
|
91
|
+
const persistedMap = new Map(persistedRows.map((row)=>[
|
|
92
|
+
row.noteId,
|
|
93
|
+
row
|
|
94
|
+
]));
|
|
95
|
+
return posts.map((post)=>{
|
|
96
|
+
const persisted = persistedMap.get(post.id);
|
|
97
|
+
return {
|
|
98
|
+
id: persisted?.id ?? post.id,
|
|
99
|
+
title: persisted?.title ?? post.noteCard.displayTitle ?? '',
|
|
100
|
+
like: persisted?.likeCount ?? post.noteCard.interactInfo.likedCount ?? ''
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
87
104
|
async function getOrCreateXiaohongshuPage(session) {
|
|
88
105
|
return session.page;
|
|
89
106
|
}
|
|
@@ -147,20 +164,32 @@ async function collectHomeFeedItems(page) {
|
|
|
147
164
|
waitUntil: 'domcontentloaded'
|
|
148
165
|
});
|
|
149
166
|
}
|
|
167
|
+
await simulateMousePresence(page);
|
|
150
168
|
await page.waitForTimeout(500);
|
|
151
|
-
|
|
169
|
+
const feedItems = await feedPromise;
|
|
170
|
+
await simulateMousePresence(page);
|
|
171
|
+
return feedItems;
|
|
152
172
|
}
|
|
153
|
-
export async function getRednoteHomePosts(session) {
|
|
173
|
+
export async function getRednoteHomePosts(session, instanceName) {
|
|
154
174
|
const page = await getOrCreateXiaohongshuPage(session);
|
|
155
175
|
const items = await collectHomeFeedItems(page);
|
|
156
176
|
const posts = items.map(normalizeHomePost);
|
|
177
|
+
let summaries = buildPostSummaryList(posts);
|
|
178
|
+
if (instanceName) {
|
|
179
|
+
await persistHomePosts(instanceName, posts.map((post, index)=>({
|
|
180
|
+
post,
|
|
181
|
+
raw: items[index] ?? post
|
|
182
|
+
})));
|
|
183
|
+
summaries = buildPostSummaryList(posts, await listPersistedPostSummaries(instanceName, posts.map((post)=>post.id)));
|
|
184
|
+
}
|
|
157
185
|
return {
|
|
158
186
|
ok: true,
|
|
159
187
|
home: {
|
|
160
188
|
pageUrl: page.url(),
|
|
161
189
|
fetchedAt: new Date().toISOString(),
|
|
162
190
|
total: posts.length,
|
|
163
|
-
posts
|
|
191
|
+
posts,
|
|
192
|
+
summaries
|
|
164
193
|
}
|
|
165
194
|
};
|
|
166
195
|
}
|
|
@@ -169,21 +198,16 @@ function writeHomeOutput(result, values) {
|
|
|
169
198
|
const savedPath = resolveJsonSavePath(values.savePath);
|
|
170
199
|
result.home.savedPath = savedPath;
|
|
171
200
|
writeJsonFile(result.home.posts, savedPath);
|
|
172
|
-
process.stdout.write(
|
|
201
|
+
process.stdout.write(renderPostSummaryList(result.home.summaries));
|
|
173
202
|
return;
|
|
174
203
|
}
|
|
175
204
|
const posts = result.home.posts;
|
|
176
|
-
let savedPath;
|
|
177
205
|
if (values.saveRequested) {
|
|
178
|
-
savedPath = resolveSavePath('home', values.savePath);
|
|
206
|
+
const savedPath = resolveSavePath('home', values.savePath);
|
|
179
207
|
writePostsJsonl(posts, savedPath);
|
|
180
208
|
result.home.savedPath = savedPath;
|
|
181
209
|
}
|
|
182
|
-
|
|
183
|
-
if (savedPath) {
|
|
184
|
-
markdown = `Saved JSONL: ${savedPath}\n\n${markdown}`;
|
|
185
|
-
}
|
|
186
|
-
process.stdout.write(markdown);
|
|
210
|
+
process.stdout.write(renderPostSummaryList(result.home.summaries));
|
|
187
211
|
}
|
|
188
212
|
export async function runHomeCommand(values = {
|
|
189
213
|
format: 'md',
|
|
@@ -194,10 +218,11 @@ export async function runHomeCommand(values = {
|
|
|
194
218
|
return;
|
|
195
219
|
}
|
|
196
220
|
ensureJsonSavePath(values.format, values.savePath);
|
|
221
|
+
await initializeRednoteDatabase();
|
|
197
222
|
const target = resolveStatusTarget(values.instance);
|
|
198
223
|
const session = await createRednoteSession(target);
|
|
199
224
|
try {
|
|
200
|
-
const result = await getRednoteHomePosts(session);
|
|
225
|
+
const result = await getRednoteHomePosts(session, target.instanceName);
|
|
201
226
|
writeHomeOutput(result, values);
|
|
202
227
|
} finally{
|
|
203
228
|
await disconnectRednoteSession(session);
|
package/dist/rednote/index.js
CHANGED
|
@@ -11,7 +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
|
+
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]
|
|
@@ -25,7 +25,7 @@ Examples:
|
|
|
25
25
|
npx -y @skills-store/rednote status --instance seller-main
|
|
26
26
|
npx -y @skills-store/rednote login --instance seller-main
|
|
27
27
|
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 --
|
|
28
|
+
npx -y @skills-store/rednote interact --instance seller-main --id NOTE_ID --like --collect --comment "Great post"
|
|
29
29
|
npx -y @skills-store/rednote home --instance seller-main --format md --save
|
|
30
30
|
npx -y @skills-store/rednote search --instance seller-main --keyword skincare --format json --save ./output/search.json
|
|
31
31
|
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
|
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{
|
|
@@ -175,6 +175,16 @@ export function renderJsonSaveSummary(filePath, payload) {
|
|
|
175
175
|
function formatField(value) {
|
|
176
176
|
return value ?? '';
|
|
177
177
|
}
|
|
178
|
+
export function renderPostSummaryList(items) {
|
|
179
|
+
if (items.length === 0) {
|
|
180
|
+
return 'No posts were captured.\n';
|
|
181
|
+
}
|
|
182
|
+
return `${items.map((item)=>[
|
|
183
|
+
`id=${item.id}`,
|
|
184
|
+
`title=${item.title}`,
|
|
185
|
+
`like=${item.like}`
|
|
186
|
+
].join('\n')).join('\n\n')}\n`;
|
|
187
|
+
}
|
|
178
188
|
export function renderPostsMarkdown(posts) {
|
|
179
189
|
if (posts.length === 0) {
|
|
180
190
|
return 'No posts were captured.\n';
|