@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.
@@ -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
+ }
@@ -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 { ensureJsonSavePath, parseOutputCliArgs, renderJsonSaveSummary, renderPostsMarkdown, resolveJsonSavePath, resolveSavePath, writeJsonFile, writePostsJsonl } from './output-format.js';
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: xsecToken ? `https://www.xiaohongshu.com/explore/${item.id}?xsec_token=${xsecToken}` : `https://www.xiaohongshu.com/explore/${item.id}`,
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
- return await searchPromise;
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(renderJsonSaveSummary(savedPath, posts));
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
- let markdown = renderPostsMarkdown(posts);
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);