@skills-store/rednote 0.1.10 → 0.1.12
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 +2 -1
- package/dist/rednote/env.js +19 -6
- package/dist/rednote/getFeedDetail.js +234 -103
- package/dist/rednote/getProfile.js +47 -32
- package/dist/rednote/home.js +11 -7
- package/dist/rednote/index.js +9 -8
- package/dist/rednote/interact.js +4 -4
- package/dist/rednote/login.js +81 -4
- package/dist/rednote/output-format.js +75 -1
- package/dist/rednote/publish.js +38 -26
- package/dist/rednote/search.js +31 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -333,6 +333,7 @@ Use these shapes as the success model when a command returns JSON.
|
|
|
333
333
|
"loginClicked": true,
|
|
334
334
|
"pageUrl": "string",
|
|
335
335
|
"waitingForPhoneLogin": true,
|
|
336
|
+
"qrCodePath": "string|null",
|
|
336
337
|
"message": "string"
|
|
337
338
|
}
|
|
338
339
|
}
|
|
@@ -490,7 +491,7 @@ If a command fails, check these in order:
|
|
|
490
491
|
|
|
491
492
|
## Repository
|
|
492
493
|
|
|
493
|
-
- Homepage: https://github.com/skills-router/skills-store/tree/main/packages/rednote
|
|
494
|
+
- Homepage: https://github.com/skills-router/skills-store/tree/main/packages/rednote-cli
|
|
494
495
|
- Issues: https://github.com/skills-router/skills-store/issues
|
|
495
496
|
|
|
496
497
|
## License
|
package/dist/rednote/env.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
-
import {
|
|
3
|
+
import { runCli } from '../utils/browser-cli.js';
|
|
4
4
|
import { getRednoteEnvironmentInfo } from '../utils/browser-core.js';
|
|
5
|
+
import { ensureJsonSavePath, renderJsonSaveSummary, resolveJsonSavePath, writeJsonFile } from './output-format.js';
|
|
5
6
|
function printEnvHelp() {
|
|
6
7
|
process.stdout.write(`rednote env
|
|
7
8
|
|
|
8
9
|
Usage:
|
|
9
|
-
npx -y @skills-store/rednote env [--format md|json]
|
|
10
|
-
node --experimental-strip-types ./scripts/rednote/env.ts [--format md|json]
|
|
11
|
-
bun ./scripts/rednote/env.ts [--format md|json]
|
|
10
|
+
npx -y @skills-store/rednote env [--format md|json] [--save PATH]
|
|
11
|
+
node --experimental-strip-types ./scripts/rednote/env.ts [--format md|json] [--save PATH]
|
|
12
|
+
bun ./scripts/rednote/env.ts [--format md|json] [--save PATH]
|
|
12
13
|
|
|
13
14
|
Options:
|
|
14
15
|
--format FORMAT Output format: md | json. Default: md
|
|
16
|
+
--save PATH Required when --format json is used. Saves the full result as JSON
|
|
15
17
|
-h, --help Show this help
|
|
16
18
|
`);
|
|
17
19
|
}
|
|
@@ -40,8 +42,12 @@ export async function runEnvCommand(values = {}) {
|
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
const format = values.format ?? 'md';
|
|
45
|
+
ensureJsonSavePath(format, values.savePath);
|
|
43
46
|
if (format === 'json') {
|
|
44
|
-
|
|
47
|
+
const result = getRednoteEnvironmentInfo();
|
|
48
|
+
const savedPath = resolveJsonSavePath(values.savePath);
|
|
49
|
+
writeJsonFile(result, savedPath);
|
|
50
|
+
process.stdout.write(renderJsonSaveSummary(savedPath, result));
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
47
53
|
process.stdout.write(renderEnvironmentMarkdown());
|
|
@@ -55,6 +61,9 @@ async function main() {
|
|
|
55
61
|
format: {
|
|
56
62
|
type: 'string'
|
|
57
63
|
},
|
|
64
|
+
save: {
|
|
65
|
+
type: 'string'
|
|
66
|
+
},
|
|
58
67
|
help: {
|
|
59
68
|
type: 'boolean',
|
|
60
69
|
short: 'h'
|
|
@@ -64,6 +73,10 @@ async function main() {
|
|
|
64
73
|
if (values.format && values.format !== 'md' && values.format !== 'json') {
|
|
65
74
|
throw new Error(`Invalid --format value: ${String(values.format)}`);
|
|
66
75
|
}
|
|
67
|
-
await runEnvCommand(
|
|
76
|
+
await runEnvCommand({
|
|
77
|
+
format: values.format,
|
|
78
|
+
savePath: values.save,
|
|
79
|
+
help: values.help
|
|
80
|
+
});
|
|
68
81
|
}
|
|
69
82
|
runCli(import.meta.url, main);
|
|
@@ -2,59 +2,125 @@
|
|
|
2
2
|
import * as cheerio from 'cheerio';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import vm from 'node:vm';
|
|
5
|
-
import {
|
|
5
|
+
import { runCli } from '../utils/browser-cli.js';
|
|
6
6
|
import { resolveStatusTarget } from './status.js';
|
|
7
7
|
import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
|
|
8
|
+
import { ensureJsonSavePath, renderJsonSaveSummary, resolveJsonSavePath, writeJsonFile } from './output-format.js';
|
|
8
9
|
function printGetFeedDetailHelp() {
|
|
9
10
|
process.stdout.write(`rednote get-feed-detail
|
|
10
11
|
|
|
11
12
|
Usage:
|
|
12
|
-
npx -y @skills-store/rednote get-feed-detail [--instance NAME] --url URL [--url URL] [--format md|json]
|
|
13
|
-
node --experimental-strip-types ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--format md|json]
|
|
14
|
-
bun ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--format md|json]
|
|
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
16
|
|
|
16
17
|
Options:
|
|
17
18
|
--instance NAME Optional. Defaults to the saved lastConnect instance
|
|
18
19
|
--url URL Required. Xiaohongshu explore url, repeatable
|
|
20
|
+
--comments [COUNT] Optional. Include comment data. When COUNT is provided, scroll \`.note-scroller\` until COUNT comments, the end, or timeout
|
|
19
21
|
--format FORMAT Output format: md | json. Default: md
|
|
22
|
+
--save PATH Required when --format json is used. Saves the selected result array as JSON
|
|
20
23
|
-h, --help Show this help
|
|
21
24
|
`);
|
|
22
25
|
}
|
|
26
|
+
function parseOptionWithEquals(arg) {
|
|
27
|
+
const equalIndex = arg.indexOf('=');
|
|
28
|
+
if (equalIndex === -1) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
key: arg.slice(0, equalIndex),
|
|
33
|
+
value: arg.slice(equalIndex + 1)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function parseCommentsValue(value) {
|
|
37
|
+
if (!value) {
|
|
38
|
+
throw new Error('Missing value for --comments');
|
|
39
|
+
}
|
|
40
|
+
const parsed = Number(value);
|
|
41
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
42
|
+
throw new Error(`Invalid --comments value: ${String(value)}`);
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
23
46
|
export function parseGetFeedDetailCliArgs(argv) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
const values = {
|
|
48
|
+
urls: [],
|
|
49
|
+
format: 'md',
|
|
50
|
+
comments: undefined,
|
|
51
|
+
help: false
|
|
52
|
+
};
|
|
53
|
+
const positionals = [];
|
|
54
|
+
for(let index = 0; index < argv.length; index += 1){
|
|
55
|
+
const arg = argv[index];
|
|
56
|
+
const withEquals = parseOptionWithEquals(arg);
|
|
57
|
+
if (arg === '-h' || arg === '--help') {
|
|
58
|
+
values.help = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (withEquals?.key === '--instance') {
|
|
62
|
+
values.instance = withEquals.value;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === '--instance') {
|
|
66
|
+
values.instance = argv[index + 1];
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (withEquals?.key === '--url') {
|
|
71
|
+
values.urls.push(withEquals.value);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg === '--url') {
|
|
75
|
+
const nextArg = argv[index + 1];
|
|
76
|
+
if (!nextArg || nextArg.startsWith('-')) {
|
|
77
|
+
throw new Error('Missing required option value: --url');
|
|
78
|
+
}
|
|
79
|
+
values.urls.push(nextArg);
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (withEquals?.key === '--format') {
|
|
84
|
+
values.format = withEquals.value;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg === '--format') {
|
|
88
|
+
values.format = argv[index + 1];
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (withEquals?.key === '--comments') {
|
|
93
|
+
values.comments = withEquals.value ? parseCommentsValue(withEquals.value) : null;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (arg === '--comments') {
|
|
97
|
+
const nextArg = argv[index + 1];
|
|
98
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
99
|
+
values.comments = parseCommentsValue(nextArg);
|
|
100
|
+
index += 1;
|
|
101
|
+
} else {
|
|
102
|
+
values.comments = null;
|
|
42
103
|
}
|
|
104
|
+
continue;
|
|
43
105
|
}
|
|
44
|
-
|
|
106
|
+
if (withEquals?.key === '--save') {
|
|
107
|
+
values.savePath = withEquals.value;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (arg === '--save') {
|
|
111
|
+
values.savePath = argv[index + 1];
|
|
112
|
+
index += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
positionals.push(arg);
|
|
116
|
+
}
|
|
45
117
|
if (positionals.length > 0) {
|
|
46
118
|
throw new Error(`Unexpected positional arguments: ${positionals.join(' ')}`);
|
|
47
119
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
throw new Error(`Invalid --format value: ${String(format)}`);
|
|
120
|
+
if (values.format !== 'md' && values.format !== 'json') {
|
|
121
|
+
throw new Error(`Invalid --format value: ${String(values.format)}`);
|
|
51
122
|
}
|
|
52
|
-
return
|
|
53
|
-
instance: values.instance,
|
|
54
|
-
urls: values.url ?? [],
|
|
55
|
-
format,
|
|
56
|
-
help: values.help
|
|
57
|
-
};
|
|
123
|
+
return values;
|
|
58
124
|
}
|
|
59
125
|
function validateFeedDetailUrl(url) {
|
|
60
126
|
try {
|
|
@@ -94,103 +160,150 @@ function extractVideoUrl(note) {
|
|
|
94
160
|
const firstAvailable = streams.find((items)=>Array.isArray(items) && items.length > 0);
|
|
95
161
|
return firstAvailable?.[0]?.backupUrls?.[0] ?? null;
|
|
96
162
|
}
|
|
163
|
+
const COMMENTS_CONTAINER_SELECTOR = '.note-scroller';
|
|
164
|
+
const COMMENT_SCROLL_TIMEOUT_MS = 20_000;
|
|
165
|
+
const COMMENT_SCROLL_IDLE_LIMIT = 4;
|
|
166
|
+
function hasCommentsEnabled(comments) {
|
|
167
|
+
return comments !== undefined;
|
|
168
|
+
}
|
|
169
|
+
function buildCommentKey(comment) {
|
|
170
|
+
return String(comment?.id ?? comment?.commentId ?? comment?.comment_id ?? `${comment?.userInfo?.userId ?? comment?.user_info?.user_id ?? 'unknown'}:${comment?.createTime ?? comment?.create_time ?? 'unknown'}:${comment?.content ?? ''}`);
|
|
171
|
+
}
|
|
172
|
+
function getCommentCount(commentsMap) {
|
|
173
|
+
return commentsMap.size;
|
|
174
|
+
}
|
|
175
|
+
async function scrollCommentsContainer(page, targetCount, getCount) {
|
|
176
|
+
const container = page.locator(COMMENTS_CONTAINER_SELECTOR).first();
|
|
177
|
+
const visible = await container.isVisible().catch(()=>false);
|
|
178
|
+
if (!visible) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await container.scrollIntoViewIfNeeded().catch(()=>{});
|
|
182
|
+
await container.hover().catch(()=>{});
|
|
183
|
+
const getMetrics = async ()=>await container.evaluate((element)=>{
|
|
184
|
+
const htmlElement = element;
|
|
185
|
+
const atBottom = htmlElement.scrollTop + htmlElement.clientHeight >= htmlElement.scrollHeight - 8;
|
|
186
|
+
return {
|
|
187
|
+
scrollTop: htmlElement.scrollTop,
|
|
188
|
+
scrollHeight: htmlElement.scrollHeight,
|
|
189
|
+
clientHeight: htmlElement.clientHeight,
|
|
190
|
+
atBottom
|
|
191
|
+
};
|
|
192
|
+
}).catch(()=>null);
|
|
193
|
+
const deadline = Date.now() + COMMENT_SCROLL_TIMEOUT_MS;
|
|
194
|
+
let idleRounds = 0;
|
|
195
|
+
while(Date.now() < deadline){
|
|
196
|
+
if (getCount() >= targetCount) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const beforeMetrics = await getMetrics();
|
|
200
|
+
if (!beforeMetrics) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const beforeCount = getCount();
|
|
204
|
+
const delta = Math.max(Math.floor(beforeMetrics.clientHeight * 0.85), 480);
|
|
205
|
+
await page.mouse.wheel(0, delta).catch(()=>{});
|
|
206
|
+
await page.waitForTimeout(900);
|
|
207
|
+
const afterMetrics = await getMetrics();
|
|
208
|
+
await page.waitForTimeout(400);
|
|
209
|
+
const afterCount = getCount();
|
|
210
|
+
const countChanged = afterCount > beforeCount;
|
|
211
|
+
const scrollMoved = Boolean(afterMetrics) && afterMetrics.scrollTop > beforeMetrics.scrollTop;
|
|
212
|
+
const reachedBottom = Boolean(afterMetrics?.atBottom);
|
|
213
|
+
if (countChanged || scrollMoved) {
|
|
214
|
+
idleRounds = 0;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
idleRounds += 1;
|
|
218
|
+
if (reachedBottom && idleRounds >= 2 || idleRounds >= COMMENT_SCROLL_IDLE_LIMIT) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
97
223
|
function normalizeDetailNote(note) {
|
|
98
224
|
return {
|
|
99
225
|
noteId: note?.noteId ?? null,
|
|
100
226
|
title: note?.title ?? null,
|
|
101
227
|
desc: note?.desc ?? null,
|
|
102
228
|
type: note?.type ?? null,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
name: tag?.name ?? null
|
|
114
|
-
})) : [],
|
|
115
|
-
imageList: Array.isArray(note?.imageList) ? note.imageList.map((image)=>({
|
|
116
|
-
urlDefault: image?.urlDefault ?? null,
|
|
117
|
-
urlPre: image?.urlPre ?? null,
|
|
118
|
-
width: image?.width ?? null,
|
|
119
|
-
height: image?.height ?? null
|
|
120
|
-
})) : [],
|
|
121
|
-
video: note?.video ? {
|
|
122
|
-
url: extractVideoUrl(note),
|
|
123
|
-
raw: note.video
|
|
124
|
-
} : null,
|
|
125
|
-
raw: note
|
|
229
|
+
liked: note?.interactInfo?.liked ?? null,
|
|
230
|
+
likedCount: note?.interactInfo?.likedCount ?? null,
|
|
231
|
+
commentCount: note?.interactInfo?.commentCount ?? null,
|
|
232
|
+
collected: note?.interactInfo?.collected ?? null,
|
|
233
|
+
collectedCount: note?.interactInfo?.collectedCount ?? null,
|
|
234
|
+
shareCount: note?.interactInfo?.shareCount ?? null,
|
|
235
|
+
followed: note?.interactInfo?.followed ?? null,
|
|
236
|
+
tagList: Array.isArray(note?.tagList) ? note.tagList.map((tag)=>tag?.name ?? null).filter((tag)=>Boolean(tag)) : [],
|
|
237
|
+
imageList: Array.isArray(note?.imageList) ? note.imageList.map((image)=>image?.urlDefault ?? null).filter((imageUrl)=>Boolean(imageUrl)) : [],
|
|
238
|
+
video: extractVideoUrl(note)
|
|
126
239
|
};
|
|
127
240
|
}
|
|
128
241
|
function normalizeComments(comments) {
|
|
129
242
|
return comments.map((comment)=>({
|
|
130
|
-
id: comment?.id ?? comment?.commentId ?? null,
|
|
131
243
|
content: comment?.content ?? null,
|
|
132
|
-
userId: comment?.userInfo?.userId ?? null,
|
|
133
|
-
nickname: comment?.userInfo?.nickname ?? null,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
244
|
+
userId: comment?.userInfo?.userId ?? comment?.user_info?.user_id ?? null,
|
|
245
|
+
nickname: comment?.userInfo?.nickname ?? comment?.user_info?.nickname ?? null,
|
|
246
|
+
create_time: comment?.createTime ?? comment?.create_time ?? null,
|
|
247
|
+
like_count: comment?.likeCount ?? comment?.like_count ?? comment?.interactInfo?.likedCount ?? null,
|
|
248
|
+
sub_comment_count: typeof (comment?.subCommentCount ?? comment?.sub_comment_count) === 'string' ? comment?.subCommentCount ?? comment?.sub_comment_count : null
|
|
137
249
|
}));
|
|
138
250
|
}
|
|
139
251
|
function formatDetailField(value) {
|
|
140
252
|
return value ?? '';
|
|
141
253
|
}
|
|
142
|
-
function renderDetailMarkdown(items) {
|
|
254
|
+
function renderDetailMarkdown(items, includeComments = false) {
|
|
143
255
|
if (items.length === 0) {
|
|
144
|
-
return '
|
|
256
|
+
return 'No feed details were captured.\n';
|
|
145
257
|
}
|
|
146
258
|
return `${items.map((item)=>{
|
|
147
259
|
const lines = [];
|
|
148
260
|
lines.push('## Note');
|
|
149
261
|
lines.push('');
|
|
150
|
-
lines.push(`- Url: ${item.url}`);
|
|
151
262
|
lines.push(`- Title: ${formatDetailField(item.note.title)}`);
|
|
152
|
-
lines.push(`-
|
|
153
|
-
lines.push(`-
|
|
154
|
-
lines.push(`-
|
|
155
|
-
lines.push(`-
|
|
156
|
-
lines.push(`-
|
|
157
|
-
lines.push(`-
|
|
158
|
-
lines.push(`-
|
|
159
|
-
lines.push(`- Tags: ${item.note.tagList.map((tag)=>tag.name ? `#${tag.name}` : '').filter(Boolean).join(' ')}`);
|
|
263
|
+
lines.push(`- Liked: ${formatDetailField(item.note.liked)}`);
|
|
264
|
+
lines.push(`- Collected: ${formatDetailField(item.note.collected)}`);
|
|
265
|
+
lines.push(`- LikedCount: ${formatDetailField(item.note.likedCount)}`);
|
|
266
|
+
lines.push(`- CommentCount: ${formatDetailField(item.note.commentCount)}`);
|
|
267
|
+
lines.push(`- CollectedCount: ${formatDetailField(item.note.collectedCount)}`);
|
|
268
|
+
lines.push(`- ShareCount: ${formatDetailField(item.note.shareCount)}`);
|
|
269
|
+
lines.push(`- Tags: ${item.note.tagList.map((tag)=>`#${tag}`).join(' ')}`);
|
|
160
270
|
lines.push('');
|
|
161
271
|
lines.push('## Content');
|
|
162
272
|
lines.push('');
|
|
163
273
|
lines.push(item.note.desc ?? '');
|
|
164
|
-
if (item.note.imageList.length > 0 || item.note.video
|
|
274
|
+
if (item.note.imageList.length > 0 || item.note.video) {
|
|
165
275
|
lines.push('');
|
|
166
276
|
lines.push('## Media');
|
|
167
277
|
lines.push('');
|
|
168
|
-
item.note.imageList.forEach((
|
|
169
|
-
|
|
170
|
-
lines.push(`- Image${index + 1}: ${image.urlDefault}`);
|
|
171
|
-
}
|
|
278
|
+
item.note.imageList.forEach((imageUrl, index)=>{
|
|
279
|
+
lines.push(`- Image${index + 1}: ${imageUrl}`);
|
|
172
280
|
});
|
|
173
|
-
if (item.note.video
|
|
174
|
-
lines.push(`- Video: ${item.note.video
|
|
281
|
+
if (item.note.video) {
|
|
282
|
+
lines.push(`- Video: ${item.note.video}`);
|
|
175
283
|
}
|
|
176
284
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
285
|
+
if (includeComments) {
|
|
286
|
+
lines.push('');
|
|
287
|
+
lines.push('## Comments');
|
|
288
|
+
lines.push('');
|
|
289
|
+
if (!item.comments || item.comments.length === 0) {
|
|
290
|
+
lines.push('- Comments not found');
|
|
291
|
+
} else {
|
|
292
|
+
item.comments.forEach((comment)=>{
|
|
293
|
+
const prefix = comment.nickname ? `${comment.nickname}: ` : '';
|
|
294
|
+
lines.push(`- ${prefix}${comment.content ?? ''}`);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
187
297
|
}
|
|
188
298
|
return lines.join('\n');
|
|
189
299
|
}).join('\n\n---\n\n')}\n`;
|
|
190
300
|
}
|
|
191
|
-
async function captureFeedDetail(page, targetUrl) {
|
|
301
|
+
async function captureFeedDetail(page, targetUrl, commentsOption = undefined) {
|
|
302
|
+
const includeComments = hasCommentsEnabled(commentsOption);
|
|
303
|
+
const commentsTarget = typeof commentsOption === 'number' ? commentsOption : null;
|
|
192
304
|
let note = null;
|
|
193
|
-
let
|
|
305
|
+
let commentsLoaded = !includeComments;
|
|
306
|
+
const commentsMap = new Map();
|
|
194
307
|
const handleResponse = async (response)=>{
|
|
195
308
|
try {
|
|
196
309
|
const url = new URL(response.url());
|
|
@@ -214,9 +327,13 @@ async function captureFeedDetail(page, targetUrl) {
|
|
|
214
327
|
note = noteState.noteDetailMap[noteState.currentNoteId]?.note ?? note;
|
|
215
328
|
}
|
|
216
329
|
});
|
|
217
|
-
} else if (url.href.includes('comment/page?')) {
|
|
330
|
+
} else if (includeComments && url.href.includes('comment/page?')) {
|
|
218
331
|
const data = await response.json();
|
|
219
|
-
|
|
332
|
+
const nextComments = Array.isArray(data?.data?.comments) ? data.data.comments : [];
|
|
333
|
+
commentsLoaded = true;
|
|
334
|
+
for (const comment of nextComments){
|
|
335
|
+
commentsMap.set(buildCommentKey(comment), comment);
|
|
336
|
+
}
|
|
220
337
|
}
|
|
221
338
|
} catch {}
|
|
222
339
|
};
|
|
@@ -227,7 +344,7 @@ async function captureFeedDetail(page, targetUrl) {
|
|
|
227
344
|
});
|
|
228
345
|
const deadline = Date.now() + 15_000;
|
|
229
346
|
while(Date.now() < deadline){
|
|
230
|
-
if (note &&
|
|
347
|
+
if (note && commentsLoaded) {
|
|
231
348
|
break;
|
|
232
349
|
}
|
|
233
350
|
await page.waitForTimeout(200);
|
|
@@ -235,22 +352,29 @@ async function captureFeedDetail(page, targetUrl) {
|
|
|
235
352
|
if (!note) {
|
|
236
353
|
throw new Error(`Failed to capture note detail: ${targetUrl}`);
|
|
237
354
|
}
|
|
355
|
+
if (includeComments && commentsTarget) {
|
|
356
|
+
await scrollCommentsContainer(page, commentsTarget, ()=>getCommentCount(commentsMap));
|
|
357
|
+
}
|
|
238
358
|
return {
|
|
239
359
|
url: targetUrl,
|
|
240
360
|
note: normalizeDetailNote(note),
|
|
241
|
-
|
|
361
|
+
...includeComments ? {
|
|
362
|
+
comments: normalizeComments([
|
|
363
|
+
...commentsMap.values()
|
|
364
|
+
])
|
|
365
|
+
} : {}
|
|
242
366
|
};
|
|
243
367
|
} finally{
|
|
244
368
|
page.off('response', handleResponse);
|
|
245
369
|
}
|
|
246
370
|
}
|
|
247
|
-
export async function getFeedDetails(session, urls) {
|
|
371
|
+
export async function getFeedDetails(session, urls, commentsOption = undefined) {
|
|
248
372
|
const page = await getOrCreateXiaohongshuPage(session);
|
|
249
373
|
const items = [];
|
|
250
374
|
for (const url of urls){
|
|
251
375
|
const normalizedUrl = normalizeFeedDetailUrl(url);
|
|
252
376
|
validateFeedDetailUrl(normalizedUrl);
|
|
253
|
-
items.push(await captureFeedDetail(page, normalizedUrl));
|
|
377
|
+
items.push(await captureFeedDetail(page, normalizedUrl, commentsOption));
|
|
254
378
|
}
|
|
255
379
|
return {
|
|
256
380
|
ok: true,
|
|
@@ -261,12 +385,18 @@ export async function getFeedDetails(session, urls) {
|
|
|
261
385
|
}
|
|
262
386
|
};
|
|
263
387
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
|
|
388
|
+
function selectFeedDetailOutput(result) {
|
|
389
|
+
return result.detail.items;
|
|
390
|
+
}
|
|
391
|
+
function writeFeedDetailOutput(result, values) {
|
|
392
|
+
const output = selectFeedDetailOutput(result);
|
|
393
|
+
if (values.format === 'json') {
|
|
394
|
+
const savedPath = resolveJsonSavePath(values.savePath);
|
|
395
|
+
writeJsonFile(output, savedPath);
|
|
396
|
+
process.stdout.write(renderJsonSaveSummary(savedPath, output));
|
|
267
397
|
return;
|
|
268
398
|
}
|
|
269
|
-
process.stdout.write(renderDetailMarkdown(result.detail.items));
|
|
399
|
+
process.stdout.write(renderDetailMarkdown(result.detail.items, hasCommentsEnabled(values.comments)));
|
|
270
400
|
}
|
|
271
401
|
export async function runGetFeedDetailCommand(values = {
|
|
272
402
|
urls: [],
|
|
@@ -276,6 +406,7 @@ export async function runGetFeedDetailCommand(values = {
|
|
|
276
406
|
printGetFeedDetailHelp();
|
|
277
407
|
return;
|
|
278
408
|
}
|
|
409
|
+
ensureJsonSavePath(values.format, values.savePath);
|
|
279
410
|
if (values.urls.length === 0) {
|
|
280
411
|
throw new Error('Missing required option: --url');
|
|
281
412
|
}
|
|
@@ -283,8 +414,8 @@ export async function runGetFeedDetailCommand(values = {
|
|
|
283
414
|
const session = await createRednoteSession(target);
|
|
284
415
|
try {
|
|
285
416
|
await ensureRednoteLoggedIn(target, 'fetching feed detail', session);
|
|
286
|
-
const result = await getFeedDetails(session, values.urls);
|
|
287
|
-
writeFeedDetailOutput(result, values
|
|
417
|
+
const result = await getFeedDetails(session, values.urls, values.comments);
|
|
418
|
+
writeFeedDetailOutput(result, values);
|
|
288
419
|
} finally{
|
|
289
420
|
await disconnectRednoteSession(session);
|
|
290
421
|
}
|