@skills-store/rednote 0.1.12 → 0.1.14
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 +42 -28
- package/bin/rednote.js +26 -0
- 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/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
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createClient } from '@libsql/client';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import { DataSource, EntitySchema, In } from 'typeorm';
|
|
5
|
+
import { REDNOTE_DATABASE_PATH, ensureDir } from '../utils/browser-core.js';
|
|
6
|
+
const rednotePostSchema = new EntitySchema({
|
|
7
|
+
name: 'RednotePostRecord',
|
|
8
|
+
tableName: 'rednote_posts',
|
|
9
|
+
columns: {
|
|
10
|
+
id: {
|
|
11
|
+
type: String,
|
|
12
|
+
primary: true,
|
|
13
|
+
length: 16
|
|
14
|
+
},
|
|
15
|
+
noteId: {
|
|
16
|
+
name: 'noteid',
|
|
17
|
+
type: String
|
|
18
|
+
},
|
|
19
|
+
title: {
|
|
20
|
+
type: String,
|
|
21
|
+
nullable: true
|
|
22
|
+
},
|
|
23
|
+
url: {
|
|
24
|
+
type: String,
|
|
25
|
+
nullable: true
|
|
26
|
+
},
|
|
27
|
+
image: {
|
|
28
|
+
type: String,
|
|
29
|
+
nullable: true
|
|
30
|
+
},
|
|
31
|
+
likeCount: {
|
|
32
|
+
name: 'like_count',
|
|
33
|
+
type: String,
|
|
34
|
+
nullable: true
|
|
35
|
+
},
|
|
36
|
+
commentCount: {
|
|
37
|
+
name: 'comment_count',
|
|
38
|
+
type: String,
|
|
39
|
+
nullable: true
|
|
40
|
+
},
|
|
41
|
+
collectedCount: {
|
|
42
|
+
name: 'collected_count',
|
|
43
|
+
type: String,
|
|
44
|
+
nullable: true
|
|
45
|
+
},
|
|
46
|
+
sharedCount: {
|
|
47
|
+
name: 'shared_count',
|
|
48
|
+
type: String,
|
|
49
|
+
nullable: true
|
|
50
|
+
},
|
|
51
|
+
authorId: {
|
|
52
|
+
name: 'author_id',
|
|
53
|
+
type: String,
|
|
54
|
+
nullable: true
|
|
55
|
+
},
|
|
56
|
+
authorNickname: {
|
|
57
|
+
name: 'author_nickname',
|
|
58
|
+
type: String,
|
|
59
|
+
nullable: true
|
|
60
|
+
},
|
|
61
|
+
modelType: {
|
|
62
|
+
name: 'model_type',
|
|
63
|
+
type: String,
|
|
64
|
+
nullable: true
|
|
65
|
+
},
|
|
66
|
+
xsecToken: {
|
|
67
|
+
name: 'xsec_token',
|
|
68
|
+
type: String,
|
|
69
|
+
nullable: true
|
|
70
|
+
},
|
|
71
|
+
instanceName: {
|
|
72
|
+
name: 'instance_name',
|
|
73
|
+
type: String
|
|
74
|
+
},
|
|
75
|
+
raw: {
|
|
76
|
+
type: 'simple-json',
|
|
77
|
+
nullable: true
|
|
78
|
+
},
|
|
79
|
+
createdAt: {
|
|
80
|
+
name: 'created_at',
|
|
81
|
+
type: Date,
|
|
82
|
+
createDate: true
|
|
83
|
+
},
|
|
84
|
+
updatedAt: {
|
|
85
|
+
name: 'updated_at',
|
|
86
|
+
type: Date,
|
|
87
|
+
updateDate: true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
indices: [
|
|
91
|
+
{
|
|
92
|
+
name: 'IDX_rednote_posts_note_instance',
|
|
93
|
+
columns: [
|
|
94
|
+
'noteId',
|
|
95
|
+
'instanceName'
|
|
96
|
+
],
|
|
97
|
+
unique: true
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'IDX_rednote_posts_instance',
|
|
101
|
+
columns: [
|
|
102
|
+
'instanceName'
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
const rednotePostDetailSchema = new EntitySchema({
|
|
108
|
+
name: 'RednotePostDetailRecord',
|
|
109
|
+
tableName: 'rednote_post_details',
|
|
110
|
+
columns: {
|
|
111
|
+
id: {
|
|
112
|
+
type: String,
|
|
113
|
+
primary: true,
|
|
114
|
+
length: 16
|
|
115
|
+
},
|
|
116
|
+
noteId: {
|
|
117
|
+
name: 'noteid',
|
|
118
|
+
type: String
|
|
119
|
+
},
|
|
120
|
+
title: {
|
|
121
|
+
type: String,
|
|
122
|
+
nullable: true
|
|
123
|
+
},
|
|
124
|
+
content: {
|
|
125
|
+
type: 'text',
|
|
126
|
+
nullable: true
|
|
127
|
+
},
|
|
128
|
+
tags: {
|
|
129
|
+
type: 'simple-json',
|
|
130
|
+
nullable: true
|
|
131
|
+
},
|
|
132
|
+
imageList: {
|
|
133
|
+
name: 'image_list',
|
|
134
|
+
type: 'simple-json',
|
|
135
|
+
nullable: true
|
|
136
|
+
},
|
|
137
|
+
videoUrl: {
|
|
138
|
+
name: 'video_url',
|
|
139
|
+
type: String,
|
|
140
|
+
nullable: true
|
|
141
|
+
},
|
|
142
|
+
likeCount: {
|
|
143
|
+
name: 'like_count',
|
|
144
|
+
type: String,
|
|
145
|
+
nullable: true
|
|
146
|
+
},
|
|
147
|
+
commentCount: {
|
|
148
|
+
name: 'comment_count',
|
|
149
|
+
type: String,
|
|
150
|
+
nullable: true
|
|
151
|
+
},
|
|
152
|
+
collectedCount: {
|
|
153
|
+
name: 'collected_count',
|
|
154
|
+
type: String,
|
|
155
|
+
nullable: true
|
|
156
|
+
},
|
|
157
|
+
shareCount: {
|
|
158
|
+
name: 'share_count',
|
|
159
|
+
type: String,
|
|
160
|
+
nullable: true
|
|
161
|
+
},
|
|
162
|
+
authorId: {
|
|
163
|
+
name: 'author_id',
|
|
164
|
+
type: String,
|
|
165
|
+
nullable: true
|
|
166
|
+
},
|
|
167
|
+
authorNickname: {
|
|
168
|
+
name: 'author_nickname',
|
|
169
|
+
type: String,
|
|
170
|
+
nullable: true
|
|
171
|
+
},
|
|
172
|
+
noteType: {
|
|
173
|
+
name: 'note_type',
|
|
174
|
+
type: String,
|
|
175
|
+
nullable: true
|
|
176
|
+
},
|
|
177
|
+
instanceName: {
|
|
178
|
+
name: 'instance_name',
|
|
179
|
+
type: String
|
|
180
|
+
},
|
|
181
|
+
raw: {
|
|
182
|
+
type: 'simple-json',
|
|
183
|
+
nullable: true
|
|
184
|
+
},
|
|
185
|
+
createdAt: {
|
|
186
|
+
name: 'created_at',
|
|
187
|
+
type: Date,
|
|
188
|
+
createDate: true
|
|
189
|
+
},
|
|
190
|
+
updatedAt: {
|
|
191
|
+
name: 'updated_at',
|
|
192
|
+
type: Date,
|
|
193
|
+
updateDate: true
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
indices: [
|
|
197
|
+
{
|
|
198
|
+
name: 'IDX_rednote_post_details_note_instance',
|
|
199
|
+
columns: [
|
|
200
|
+
'noteId',
|
|
201
|
+
'instanceName'
|
|
202
|
+
],
|
|
203
|
+
unique: true
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'IDX_rednote_post_details_instance',
|
|
207
|
+
columns: [
|
|
208
|
+
'instanceName'
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
});
|
|
213
|
+
const rednotePostCommentSchema = new EntitySchema({
|
|
214
|
+
name: 'RednotePostCommentRecord',
|
|
215
|
+
tableName: 'rednote_post_comments',
|
|
216
|
+
columns: {
|
|
217
|
+
id: {
|
|
218
|
+
type: String,
|
|
219
|
+
primary: true,
|
|
220
|
+
length: 16
|
|
221
|
+
},
|
|
222
|
+
commentId: {
|
|
223
|
+
name: 'commentid',
|
|
224
|
+
type: String
|
|
225
|
+
},
|
|
226
|
+
noteId: {
|
|
227
|
+
name: 'noteid',
|
|
228
|
+
type: String
|
|
229
|
+
},
|
|
230
|
+
content: {
|
|
231
|
+
type: 'text',
|
|
232
|
+
nullable: true
|
|
233
|
+
},
|
|
234
|
+
likeCount: {
|
|
235
|
+
name: 'like_count',
|
|
236
|
+
type: String,
|
|
237
|
+
nullable: true
|
|
238
|
+
},
|
|
239
|
+
replyCount: {
|
|
240
|
+
name: 'reply_count',
|
|
241
|
+
type: String,
|
|
242
|
+
nullable: true
|
|
243
|
+
},
|
|
244
|
+
authorId: {
|
|
245
|
+
name: 'author_id',
|
|
246
|
+
type: String,
|
|
247
|
+
nullable: true
|
|
248
|
+
},
|
|
249
|
+
authorNickname: {
|
|
250
|
+
name: 'author_nickname',
|
|
251
|
+
type: String,
|
|
252
|
+
nullable: true
|
|
253
|
+
},
|
|
254
|
+
instanceName: {
|
|
255
|
+
name: 'instance_name',
|
|
256
|
+
type: String
|
|
257
|
+
},
|
|
258
|
+
raw: {
|
|
259
|
+
type: 'simple-json',
|
|
260
|
+
nullable: true
|
|
261
|
+
},
|
|
262
|
+
createdAt: {
|
|
263
|
+
name: 'created_at',
|
|
264
|
+
type: Date,
|
|
265
|
+
createDate: true
|
|
266
|
+
},
|
|
267
|
+
updatedAt: {
|
|
268
|
+
name: 'updated_at',
|
|
269
|
+
type: Date,
|
|
270
|
+
updateDate: true
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
indices: [
|
|
274
|
+
{
|
|
275
|
+
name: 'IDX_rednote_post_comments_unique',
|
|
276
|
+
columns: [
|
|
277
|
+
'commentId',
|
|
278
|
+
'noteId',
|
|
279
|
+
'instanceName'
|
|
280
|
+
],
|
|
281
|
+
unique: true
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'IDX_rednote_post_comments_note_instance',
|
|
285
|
+
columns: [
|
|
286
|
+
'noteId',
|
|
287
|
+
'instanceName'
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
});
|
|
292
|
+
let dataSourcePromise = null;
|
|
293
|
+
function createRecordId() {
|
|
294
|
+
return nanoid(16);
|
|
295
|
+
}
|
|
296
|
+
function uniqueStrings(values) {
|
|
297
|
+
return [
|
|
298
|
+
...new Set(values.filter((value)=>typeof value === 'string' && value.length > 0))
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
function firstNonEmpty(...values) {
|
|
302
|
+
return values.find((value)=>typeof value === 'string' && value.length > 0) ?? null;
|
|
303
|
+
}
|
|
304
|
+
function toCountString(value) {
|
|
305
|
+
if (value === null || value === undefined || value === '') {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return String(value);
|
|
309
|
+
}
|
|
310
|
+
function coalesceValue(nextValue, fallbackValue) {
|
|
311
|
+
return nextValue ?? fallbackValue ?? null;
|
|
312
|
+
}
|
|
313
|
+
function extractPrimaryImage(post) {
|
|
314
|
+
const cover = post.noteCard.cover;
|
|
315
|
+
return firstNonEmpty(cover.urlDefault, cover.urlPre, cover.url, ...post.noteCard.imageList.flatMap((image)=>image.infoList.map((info)=>info.url)));
|
|
316
|
+
}
|
|
317
|
+
function extractNoteIdFromUrl(url) {
|
|
318
|
+
try {
|
|
319
|
+
const parsed = new URL(url);
|
|
320
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
321
|
+
return segments.at(-1) ?? null;
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function extractAuthorFromRawNote(rawNote) {
|
|
327
|
+
if (!rawNote || typeof rawNote !== 'object' || Array.isArray(rawNote)) {
|
|
328
|
+
return {
|
|
329
|
+
userId: null,
|
|
330
|
+
nickname: null
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const note = rawNote;
|
|
334
|
+
const user = note.user ?? note.userInfo ?? note.user_info;
|
|
335
|
+
return {
|
|
336
|
+
userId: user?.userId ?? user?.user_id ?? null,
|
|
337
|
+
nickname: user?.nickname ?? user?.nickName ?? user?.nick_name ?? null
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function extractCommentId(comment) {
|
|
341
|
+
const value = comment?.id ?? comment?.commentId ?? comment?.comment_id;
|
|
342
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
343
|
+
return String(value);
|
|
344
|
+
}
|
|
345
|
+
const fallbackUserId = comment?.userInfo?.userId ?? comment?.user_info?.user_id ?? 'unknown';
|
|
346
|
+
const fallbackCreateTime = comment?.createTime ?? comment?.create_time ?? 'unknown';
|
|
347
|
+
const fallbackContent = comment?.content ?? '';
|
|
348
|
+
return `${fallbackUserId}:${fallbackCreateTime}:${fallbackContent}`;
|
|
349
|
+
}
|
|
350
|
+
async function initializeDataSource() {
|
|
351
|
+
ensureDir(path.dirname(REDNOTE_DATABASE_PATH));
|
|
352
|
+
const client = createClient({
|
|
353
|
+
url: `file:${REDNOTE_DATABASE_PATH}`
|
|
354
|
+
});
|
|
355
|
+
await client.execute('SELECT 1');
|
|
356
|
+
client.close();
|
|
357
|
+
const dataSource = new DataSource({
|
|
358
|
+
type: 'better-sqlite3',
|
|
359
|
+
database: REDNOTE_DATABASE_PATH,
|
|
360
|
+
entities: [
|
|
361
|
+
rednotePostSchema,
|
|
362
|
+
rednotePostDetailSchema,
|
|
363
|
+
rednotePostCommentSchema
|
|
364
|
+
],
|
|
365
|
+
synchronize: true,
|
|
366
|
+
logging: false,
|
|
367
|
+
prepareDatabase: (database)=>{
|
|
368
|
+
database.pragma('journal_mode = WAL');
|
|
369
|
+
database.pragma('foreign_keys = ON');
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
return await dataSource.initialize();
|
|
373
|
+
}
|
|
374
|
+
export async function initializeRednoteDatabase() {
|
|
375
|
+
if (!dataSourcePromise) {
|
|
376
|
+
dataSourcePromise = initializeDataSource().catch((error)=>{
|
|
377
|
+
dataSourcePromise = null;
|
|
378
|
+
throw error;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return await dataSourcePromise;
|
|
382
|
+
}
|
|
383
|
+
async function persistPosts(instanceName, inputs) {
|
|
384
|
+
if (inputs.length === 0) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const dataSource = await initializeRednoteDatabase();
|
|
388
|
+
const repository = dataSource.getRepository(rednotePostSchema);
|
|
389
|
+
const noteIds = uniqueStrings(inputs.map((input)=>input.post.id));
|
|
390
|
+
const existingRows = noteIds.length > 0 ? await repository.find({
|
|
391
|
+
where: {
|
|
392
|
+
instanceName,
|
|
393
|
+
noteId: In(noteIds)
|
|
394
|
+
}
|
|
395
|
+
}) : [];
|
|
396
|
+
const existingMap = new Map(existingRows.map((row)=>[
|
|
397
|
+
row.noteId,
|
|
398
|
+
row
|
|
399
|
+
]));
|
|
400
|
+
const entities = inputs.map(({ post, raw })=>{
|
|
401
|
+
const existing = existingMap.get(post.id);
|
|
402
|
+
const image = extractPrimaryImage(post);
|
|
403
|
+
const authorNickname = firstNonEmpty(post.noteCard.user.nickname, post.noteCard.user.nickName);
|
|
404
|
+
return repository.create({
|
|
405
|
+
id: existing?.id ?? createRecordId(),
|
|
406
|
+
noteId: post.id,
|
|
407
|
+
title: coalesceValue(post.noteCard.displayTitle, existing?.title),
|
|
408
|
+
url: coalesceValue(post.url, existing?.url),
|
|
409
|
+
image: coalesceValue(image, existing?.image),
|
|
410
|
+
likeCount: coalesceValue(toCountString(post.noteCard.interactInfo.likedCount), existing?.likeCount),
|
|
411
|
+
commentCount: coalesceValue(toCountString(post.noteCard.interactInfo.commentCount), existing?.commentCount),
|
|
412
|
+
collectedCount: coalesceValue(toCountString(post.noteCard.interactInfo.collectedCount), existing?.collectedCount),
|
|
413
|
+
sharedCount: coalesceValue(toCountString(post.noteCard.interactInfo.sharedCount), existing?.sharedCount),
|
|
414
|
+
authorId: coalesceValue(post.noteCard.user.userId, existing?.authorId),
|
|
415
|
+
authorNickname: coalesceValue(authorNickname, existing?.authorNickname),
|
|
416
|
+
modelType: coalesceValue(post.modelType, existing?.modelType),
|
|
417
|
+
xsecToken: coalesceValue(post.xsecToken, existing?.xsecToken),
|
|
418
|
+
instanceName,
|
|
419
|
+
raw,
|
|
420
|
+
...existing?.createdAt ? {
|
|
421
|
+
createdAt: existing.createdAt
|
|
422
|
+
} : {}
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
await repository.save(entities);
|
|
426
|
+
}
|
|
427
|
+
export async function persistHomePosts(instanceName, inputs) {
|
|
428
|
+
await persistPosts(instanceName, inputs);
|
|
429
|
+
}
|
|
430
|
+
export async function persistSearchPosts(instanceName, inputs) {
|
|
431
|
+
await persistPosts(instanceName, inputs);
|
|
432
|
+
}
|
|
433
|
+
export async function persistFeedDetail(input) {
|
|
434
|
+
const noteId = input.note.noteId ?? extractNoteIdFromUrl(input.url);
|
|
435
|
+
if (!noteId) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const dataSource = await initializeRednoteDatabase();
|
|
439
|
+
await dataSource.transaction(async (manager)=>{
|
|
440
|
+
const postRepository = manager.getRepository(rednotePostSchema);
|
|
441
|
+
const detailRepository = manager.getRepository(rednotePostDetailSchema);
|
|
442
|
+
const commentRepository = manager.getRepository(rednotePostCommentSchema);
|
|
443
|
+
const [existingPost, existingDetail] = await Promise.all([
|
|
444
|
+
postRepository.findOne({
|
|
445
|
+
where: {
|
|
446
|
+
instanceName: input.instanceName,
|
|
447
|
+
noteId
|
|
448
|
+
}
|
|
449
|
+
}),
|
|
450
|
+
detailRepository.findOne({
|
|
451
|
+
where: {
|
|
452
|
+
instanceName: input.instanceName,
|
|
453
|
+
noteId
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
]);
|
|
457
|
+
const author = extractAuthorFromRawNote(input.rawNote);
|
|
458
|
+
await postRepository.save(postRepository.create({
|
|
459
|
+
id: existingPost?.id ?? createRecordId(),
|
|
460
|
+
noteId,
|
|
461
|
+
title: coalesceValue(input.note.title, existingPost?.title),
|
|
462
|
+
url: coalesceValue(input.url, existingPost?.url),
|
|
463
|
+
image: coalesceValue(input.note.imageList[0] ?? null, existingPost?.image),
|
|
464
|
+
likeCount: coalesceValue(toCountString(input.note.likedCount), existingPost?.likeCount),
|
|
465
|
+
commentCount: coalesceValue(toCountString(input.note.commentCount), existingPost?.commentCount),
|
|
466
|
+
collectedCount: coalesceValue(toCountString(input.note.collectedCount), existingPost?.collectedCount),
|
|
467
|
+
sharedCount: coalesceValue(toCountString(input.note.shareCount), existingPost?.sharedCount),
|
|
468
|
+
authorId: coalesceValue(author.userId, existingPost?.authorId),
|
|
469
|
+
authorNickname: coalesceValue(author.nickname, existingPost?.authorNickname),
|
|
470
|
+
modelType: coalesceValue(input.note.type, existingPost?.modelType),
|
|
471
|
+
xsecToken: coalesceValue((()=>{
|
|
472
|
+
try {
|
|
473
|
+
return new URL(input.url).searchParams.get('xsec_token');
|
|
474
|
+
} catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
})(), existingPost?.xsecToken),
|
|
478
|
+
instanceName: input.instanceName,
|
|
479
|
+
raw: input.rawNote,
|
|
480
|
+
...existingPost?.createdAt ? {
|
|
481
|
+
createdAt: existingPost.createdAt
|
|
482
|
+
} : {}
|
|
483
|
+
}));
|
|
484
|
+
await detailRepository.save(detailRepository.create({
|
|
485
|
+
id: existingDetail?.id ?? createRecordId(),
|
|
486
|
+
noteId,
|
|
487
|
+
title: coalesceValue(input.note.title, existingDetail?.title),
|
|
488
|
+
content: coalesceValue(input.note.desc, existingDetail?.content),
|
|
489
|
+
tags: input.note.tagList.length > 0 ? input.note.tagList : existingDetail?.tags ?? [],
|
|
490
|
+
imageList: input.note.imageList.length > 0 ? input.note.imageList : existingDetail?.imageList ?? [],
|
|
491
|
+
videoUrl: coalesceValue(input.note.video, existingDetail?.videoUrl),
|
|
492
|
+
likeCount: coalesceValue(toCountString(input.note.likedCount), existingDetail?.likeCount),
|
|
493
|
+
commentCount: coalesceValue(toCountString(input.note.commentCount), existingDetail?.commentCount),
|
|
494
|
+
collectedCount: coalesceValue(toCountString(input.note.collectedCount), existingDetail?.collectedCount),
|
|
495
|
+
shareCount: coalesceValue(toCountString(input.note.shareCount), existingDetail?.shareCount),
|
|
496
|
+
authorId: coalesceValue(author.userId, existingDetail?.authorId),
|
|
497
|
+
authorNickname: coalesceValue(author.nickname, existingDetail?.authorNickname),
|
|
498
|
+
noteType: coalesceValue(input.note.type, existingDetail?.noteType),
|
|
499
|
+
instanceName: input.instanceName,
|
|
500
|
+
raw: input.rawNote,
|
|
501
|
+
...existingDetail?.createdAt ? {
|
|
502
|
+
createdAt: existingDetail.createdAt
|
|
503
|
+
} : {}
|
|
504
|
+
}));
|
|
505
|
+
const rawComments = Array.isArray(input.rawComments) ? input.rawComments : [];
|
|
506
|
+
if (rawComments.length === 0) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const commentIds = rawComments.map(extractCommentId);
|
|
510
|
+
const existingComments = await commentRepository.find({
|
|
511
|
+
where: {
|
|
512
|
+
instanceName: input.instanceName,
|
|
513
|
+
noteId,
|
|
514
|
+
commentId: In(commentIds)
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
const existingCommentMap = new Map(existingComments.map((comment)=>[
|
|
518
|
+
comment.commentId,
|
|
519
|
+
comment
|
|
520
|
+
]));
|
|
521
|
+
const commentEntities = rawComments.map((comment)=>{
|
|
522
|
+
const commentId = extractCommentId(comment);
|
|
523
|
+
const existing = existingCommentMap.get(commentId);
|
|
524
|
+
return commentRepository.create({
|
|
525
|
+
id: existing?.id ?? createRecordId(),
|
|
526
|
+
commentId,
|
|
527
|
+
noteId,
|
|
528
|
+
content: coalesceValue(comment?.content ?? null, existing?.content),
|
|
529
|
+
likeCount: coalesceValue(toCountString(comment?.likeCount ?? comment?.like_count ?? comment?.interactInfo?.likedCount), existing?.likeCount),
|
|
530
|
+
replyCount: coalesceValue(toCountString(comment?.subCommentCount ?? comment?.sub_comment_count), existing?.replyCount),
|
|
531
|
+
authorId: coalesceValue(comment?.userInfo?.userId ?? comment?.user_info?.user_id ?? null, existing?.authorId),
|
|
532
|
+
authorNickname: coalesceValue(comment?.userInfo?.nickname ?? comment?.user_info?.nickname ?? null, existing?.authorNickname),
|
|
533
|
+
instanceName: input.instanceName,
|
|
534
|
+
raw: comment,
|
|
535
|
+
...existing?.createdAt ? {
|
|
536
|
+
createdAt: existing.createdAt
|
|
537
|
+
} : {}
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
await commentRepository.save(commentEntities);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
export async function listPersistedPostSummaries(instanceName, noteIds) {
|
|
544
|
+
const uniqueNoteIds = uniqueStrings(noteIds);
|
|
545
|
+
if (uniqueNoteIds.length === 0) {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
const dataSource = await initializeRednoteDatabase();
|
|
549
|
+
const repository = dataSource.getRepository(rednotePostSchema);
|
|
550
|
+
const rows = await repository.find({
|
|
551
|
+
where: {
|
|
552
|
+
instanceName,
|
|
553
|
+
noteId: In(uniqueNoteIds)
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
const rowMap = new Map(rows.map((row)=>[
|
|
557
|
+
row.noteId,
|
|
558
|
+
row
|
|
559
|
+
]));
|
|
560
|
+
return uniqueNoteIds.map((noteId)=>rowMap.get(noteId)).filter((row)=>Boolean(row)).map((row)=>({
|
|
561
|
+
id: row.id,
|
|
562
|
+
noteId: row.noteId,
|
|
563
|
+
title: row.title,
|
|
564
|
+
likeCount: row.likeCount,
|
|
565
|
+
url: row.url
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
export async function findPersistedPostUrlByRecordId(instanceName, id) {
|
|
569
|
+
const dataSource = await initializeRednoteDatabase();
|
|
570
|
+
const repository = dataSource.getRepository(rednotePostSchema);
|
|
571
|
+
const row = await repository.findOne({
|
|
572
|
+
where: {
|
|
573
|
+
instanceName,
|
|
574
|
+
id
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
return row?.url ?? null;
|
|
578
|
+
}
|
package/dist/rednote/search.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runCli } from '../utils/browser-cli.js';
|
|
3
|
+
import { simulateMousePresence } from '../utils/mouse-helper.js';
|
|
3
4
|
import { resolveStatusTarget } from './status.js';
|
|
4
5
|
import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
|
|
5
|
-
import {
|
|
6
|
+
import { initializeRednoteDatabase, listPersistedPostSummaries, persistSearchPosts } from './persistence.js';
|
|
7
|
+
import { ensureJsonSavePath, parseOutputCliArgs, renderPostSummaryList, resolveJsonSavePath, resolveSavePath, writeJsonFile, writePostsJsonl } from './output-format.js';
|
|
6
8
|
export function parseSearchCliArgs(argv) {
|
|
7
9
|
return parseOutputCliArgs(argv, {
|
|
8
10
|
includeKeyword: true
|
|
@@ -32,11 +34,12 @@ function normalizeSearchPost(item) {
|
|
|
32
34
|
const imageList = Array.isArray(noteCard.image_list) ? noteCard.image_list : [];
|
|
33
35
|
const cornerTagInfo = Array.isArray(noteCard.corner_tag_info) ? noteCard.corner_tag_info : [];
|
|
34
36
|
const xsecToken = item.xsec_token ?? 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.model_type,
|
|
38
41
|
xsecToken,
|
|
39
|
-
url
|
|
42
|
+
url,
|
|
40
43
|
noteCard: {
|
|
41
44
|
type: noteCard.type ?? null,
|
|
42
45
|
displayTitle: noteCard.display_title ?? null,
|
|
@@ -84,6 +87,20 @@ function normalizeSearchPost(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
|
function toSearchJsonPost(post) {
|
|
88
105
|
return {
|
|
89
106
|
id: post.id,
|
|
@@ -155,16 +172,29 @@ async function collectSearchItems(page, keyword) {
|
|
|
155
172
|
});
|
|
156
173
|
}
|
|
157
174
|
const searchInput = page.locator('#search-input');
|
|
175
|
+
await simulateMousePresence(page, {
|
|
176
|
+
locator: searchInput
|
|
177
|
+
});
|
|
158
178
|
await searchInput.focus();
|
|
159
179
|
await searchInput.fill(keyword);
|
|
160
180
|
await page.keyboard.press('Enter');
|
|
161
181
|
await page.waitForTimeout(500);
|
|
162
|
-
|
|
182
|
+
const searchItems = await searchPromise;
|
|
183
|
+
await simulateMousePresence(page);
|
|
184
|
+
return searchItems;
|
|
163
185
|
}
|
|
164
|
-
export async function searchRednotePosts(session, keyword) {
|
|
186
|
+
export async function searchRednotePosts(session, keyword, instanceName) {
|
|
165
187
|
const page = await getOrCreateXiaohongshuPage(session);
|
|
166
188
|
const items = await collectSearchItems(page, keyword);
|
|
167
189
|
const posts = items.map(normalizeSearchPost);
|
|
190
|
+
let summaries = buildPostSummaryList(posts);
|
|
191
|
+
if (instanceName) {
|
|
192
|
+
await persistSearchPosts(instanceName, posts.map((post, index)=>({
|
|
193
|
+
post,
|
|
194
|
+
raw: items[index] ?? post
|
|
195
|
+
})));
|
|
196
|
+
summaries = buildPostSummaryList(posts, await listPersistedPostSummaries(instanceName, posts.map((post)=>post.id)));
|
|
197
|
+
}
|
|
168
198
|
return {
|
|
169
199
|
ok: true,
|
|
170
200
|
search: {
|
|
@@ -172,7 +202,8 @@ export async function searchRednotePosts(session, keyword) {
|
|
|
172
202
|
pageUrl: page.url(),
|
|
173
203
|
fetchedAt: new Date().toISOString(),
|
|
174
204
|
total: posts.length,
|
|
175
|
-
posts
|
|
205
|
+
posts,
|
|
206
|
+
summaries
|
|
176
207
|
}
|
|
177
208
|
};
|
|
178
209
|
}
|
|
@@ -181,21 +212,16 @@ function writeSearchOutput(result, values) {
|
|
|
181
212
|
const savedPath = resolveJsonSavePath(values.savePath);
|
|
182
213
|
const posts = result.search.posts.map(toSearchJsonPost);
|
|
183
214
|
writeJsonFile(posts, savedPath);
|
|
184
|
-
process.stdout.write(
|
|
215
|
+
process.stdout.write(renderPostSummaryList(result.search.summaries));
|
|
185
216
|
return;
|
|
186
217
|
}
|
|
187
218
|
const posts = result.search.posts;
|
|
188
|
-
let savedPath;
|
|
189
219
|
if (values.saveRequested) {
|
|
190
|
-
savedPath = resolveSavePath('search', values.savePath, result.search.keyword);
|
|
220
|
+
const savedPath = resolveSavePath('search', values.savePath, result.search.keyword);
|
|
191
221
|
writePostsJsonl(posts, savedPath);
|
|
192
222
|
result.search.savedPath = savedPath;
|
|
193
223
|
}
|
|
194
|
-
|
|
195
|
-
if (savedPath) {
|
|
196
|
-
markdown = `Saved JSONL: ${savedPath}\n\n${markdown}`;
|
|
197
|
-
}
|
|
198
|
-
process.stdout.write(markdown);
|
|
224
|
+
process.stdout.write(renderPostSummaryList(result.search.summaries));
|
|
199
225
|
}
|
|
200
226
|
export async function runSearchCommand(values = {
|
|
201
227
|
format: 'md',
|
|
@@ -210,11 +236,12 @@ export async function runSearchCommand(values = {
|
|
|
210
236
|
if (!keyword) {
|
|
211
237
|
throw new Error('Missing required option: --keyword');
|
|
212
238
|
}
|
|
239
|
+
await initializeRednoteDatabase();
|
|
213
240
|
const target = resolveStatusTarget(values.instance);
|
|
214
241
|
const session = await createRednoteSession(target);
|
|
215
242
|
try {
|
|
216
243
|
await ensureRednoteLoggedIn(target, 'search', session);
|
|
217
|
-
const result = await searchRednotePosts(session, keyword);
|
|
244
|
+
const result = await searchRednotePosts(session, keyword, target.instanceName);
|
|
218
245
|
writeSearchOutput(result, values);
|
|
219
246
|
} finally{
|
|
220
247
|
await disconnectRednoteSession(session);
|