@skills-store/rednote 0.1.0
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 +35 -0
- package/bin/rednote.js +27 -0
- package/package.json +54 -0
- package/scripts/browser/connect-browser.ts +218 -0
- package/scripts/browser/create-browser.ts +81 -0
- package/scripts/browser/index.ts +49 -0
- package/scripts/browser/list-browser.ts +74 -0
- package/scripts/browser/remove-browser.ts +109 -0
- package/scripts/index.ts +54 -0
- package/scripts/rednote/checkLogin.ts +171 -0
- package/scripts/rednote/collect.ts +0 -0
- package/scripts/rednote/env.ts +79 -0
- package/scripts/rednote/getFeedDetail.ts +351 -0
- package/scripts/rednote/getProfile.ts +420 -0
- package/scripts/rednote/home.ts +316 -0
- package/scripts/rednote/index.ts +122 -0
- package/scripts/rednote/login.ts +142 -0
- package/scripts/rednote/output-format.ts +156 -0
- package/scripts/rednote/post-types.ts +51 -0
- package/scripts/rednote/publish.ts +0 -0
- package/scripts/rednote/search.ts +316 -0
- package/scripts/rednote/status.ts +280 -0
- package/scripts/utils/browser-cli.ts +176 -0
- package/scripts/utils/browser-core.ts +906 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import type { RednotePost } from './post-types.ts';
|
|
5
|
+
|
|
6
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const REDNOTE_ROOT = path.resolve(SCRIPT_DIR, '../..');
|
|
8
|
+
|
|
9
|
+
export type OutputFormat = 'json' | 'md';
|
|
10
|
+
|
|
11
|
+
export type OutputCliValues = {
|
|
12
|
+
instance?: string;
|
|
13
|
+
keyword?: string;
|
|
14
|
+
format: OutputFormat;
|
|
15
|
+
saveRequested: boolean;
|
|
16
|
+
savePath?: string;
|
|
17
|
+
help?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function parseOptionWithEquals(arg: string) {
|
|
21
|
+
const equalIndex = arg.indexOf('=');
|
|
22
|
+
if (equalIndex === -1) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
key: arg.slice(0, equalIndex),
|
|
28
|
+
value: arg.slice(equalIndex + 1),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseOutputCliArgs(argv: string[], options: { includeKeyword?: boolean } = {}): OutputCliValues {
|
|
33
|
+
const values: OutputCliValues = {
|
|
34
|
+
format: 'md',
|
|
35
|
+
saveRequested: false,
|
|
36
|
+
help: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
40
|
+
const arg = argv[index];
|
|
41
|
+
const withEquals = parseOptionWithEquals(arg);
|
|
42
|
+
|
|
43
|
+
if (arg === '-h' || arg === '--help') {
|
|
44
|
+
values.help = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (withEquals?.key === '--instance') {
|
|
49
|
+
values.instance = withEquals.value;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (arg === '--instance') {
|
|
54
|
+
values.instance = argv[index + 1];
|
|
55
|
+
index += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.includeKeyword && withEquals?.key === '--keyword') {
|
|
60
|
+
values.keyword = withEquals.value;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.includeKeyword && arg === '--keyword') {
|
|
65
|
+
values.keyword = argv[index + 1];
|
|
66
|
+
index += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (withEquals?.key === '--format') {
|
|
71
|
+
const format = withEquals.value;
|
|
72
|
+
if (format !== 'json' && format !== 'md') {
|
|
73
|
+
throw new Error(`Invalid --format value: ${format}`);
|
|
74
|
+
}
|
|
75
|
+
values.format = format;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (arg === '--format') {
|
|
80
|
+
const format = argv[index + 1];
|
|
81
|
+
if (format !== 'json' && format !== 'md') {
|
|
82
|
+
throw new Error(`Invalid --format value: ${String(format)}`);
|
|
83
|
+
}
|
|
84
|
+
values.format = format;
|
|
85
|
+
index += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (withEquals?.key === '--save') {
|
|
90
|
+
values.saveRequested = true;
|
|
91
|
+
values.savePath = withEquals.value;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (arg === '--save') {
|
|
96
|
+
values.saveRequested = true;
|
|
97
|
+
const nextArg = argv[index + 1];
|
|
98
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
99
|
+
values.savePath = nextArg;
|
|
100
|
+
index += 1;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return values;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function slugifyKeyword(keyword: string) {
|
|
110
|
+
return keyword
|
|
111
|
+
.trim()
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.replace(/\s+/g, '-')
|
|
114
|
+
.replace(/[^\p{Letter}\p{Number}-]+/gu, '')
|
|
115
|
+
.slice(0, 32) || 'query';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function timestampForFilename() {
|
|
119
|
+
return new Date().toISOString().replaceAll(':', '').replaceAll('.', '').replace('T', '-').replace('Z', 'Z');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function resolveSavePath(command: 'home' | 'search', explicitPath?: string, keyword?: string) {
|
|
123
|
+
if (explicitPath) {
|
|
124
|
+
return path.resolve(explicitPath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const keywordSuffix = keyword ? `-${slugifyKeyword(keyword)}` : '';
|
|
128
|
+
return path.join(REDNOTE_ROOT, 'output', `${command}${keywordSuffix}-${timestampForFilename()}.jsonl`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function writePostsJsonl(posts: RednotePost[], filePath: string) {
|
|
132
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
133
|
+
const content = posts.map((post) => JSON.stringify(post)).join('\n');
|
|
134
|
+
fs.writeFileSync(filePath, content ? `${content}\n` : '', 'utf8');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatField(value: string | null | undefined) {
|
|
138
|
+
return value ?? '';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function renderPostsMarkdown(posts: RednotePost[]) {
|
|
142
|
+
if (posts.length === 0) {
|
|
143
|
+
return '没有获取到帖子。\n';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${posts
|
|
147
|
+
.map((post) => [
|
|
148
|
+
`- id: ${post.id}`,
|
|
149
|
+
`- displayTitle: ${formatField(post.noteCard.displayTitle)}`,
|
|
150
|
+
`- likedCount: ${formatField(post.noteCard.interactInfo.likedCount)}`,
|
|
151
|
+
`- url: ${post.url}`,
|
|
152
|
+
`- nickName: ${formatField(post.noteCard.user.nickName)}`,
|
|
153
|
+
`- userId: ${formatField(post.noteCard.user.userId)}`,
|
|
154
|
+
].join('\n'))
|
|
155
|
+
.join('\n\n')}\n`;
|
|
156
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type RednotePost = {
|
|
2
|
+
id: string;
|
|
3
|
+
modelType: string;
|
|
4
|
+
xsecToken: string | null;
|
|
5
|
+
url: string;
|
|
6
|
+
noteCard: {
|
|
7
|
+
type: string | null;
|
|
8
|
+
displayTitle: string | null;
|
|
9
|
+
cover: {
|
|
10
|
+
urlDefault: string | null;
|
|
11
|
+
urlPre: string | null;
|
|
12
|
+
url: string | null;
|
|
13
|
+
fileId: string | null;
|
|
14
|
+
width: number | null;
|
|
15
|
+
height: number | null;
|
|
16
|
+
infoList: Array<{
|
|
17
|
+
imageScene: string | null;
|
|
18
|
+
url: string | null;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
user: {
|
|
22
|
+
userId: string | null;
|
|
23
|
+
nickname: string | null;
|
|
24
|
+
nickName: string | null;
|
|
25
|
+
avatar: string | null;
|
|
26
|
+
xsecToken: string | null;
|
|
27
|
+
};
|
|
28
|
+
interactInfo: {
|
|
29
|
+
liked: boolean;
|
|
30
|
+
likedCount: string | null;
|
|
31
|
+
commentCount: string | null;
|
|
32
|
+
collectedCount: string | null;
|
|
33
|
+
sharedCount: string | null;
|
|
34
|
+
};
|
|
35
|
+
cornerTagInfo: Array<{
|
|
36
|
+
type: string | null;
|
|
37
|
+
text: string | null;
|
|
38
|
+
}>;
|
|
39
|
+
imageList: Array<{
|
|
40
|
+
width: number | null;
|
|
41
|
+
height: number | null;
|
|
42
|
+
infoList: Array<{
|
|
43
|
+
imageScene: string | null;
|
|
44
|
+
url: string | null;
|
|
45
|
+
}>;
|
|
46
|
+
}>;
|
|
47
|
+
video: {
|
|
48
|
+
duration: number | null;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
|
|
3
|
+
import type { Page, Response } from 'playwright-core';
|
|
4
|
+
import { printJson, runCli } from '../utils/browser-cli.ts';
|
|
5
|
+
import { resolveStatusTarget } from './status.ts';
|
|
6
|
+
import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn, type RednoteSession } from './checkLogin.ts';
|
|
7
|
+
import type { RednotePost } from './post-types.ts';
|
|
8
|
+
import {
|
|
9
|
+
parseOutputCliArgs,
|
|
10
|
+
renderPostsMarkdown,
|
|
11
|
+
resolveSavePath,
|
|
12
|
+
writePostsJsonl,
|
|
13
|
+
type OutputCliValues,
|
|
14
|
+
} from './output-format.ts';
|
|
15
|
+
|
|
16
|
+
export interface XHSSearchItem {
|
|
17
|
+
id: string;
|
|
18
|
+
model_type: string;
|
|
19
|
+
xsec_token?: string;
|
|
20
|
+
note_card?: {
|
|
21
|
+
type?: string;
|
|
22
|
+
display_title?: string;
|
|
23
|
+
user?: {
|
|
24
|
+
avatar?: string;
|
|
25
|
+
user_id?: string;
|
|
26
|
+
nickname?: string;
|
|
27
|
+
nick_name?: string;
|
|
28
|
+
xsec_token?: string;
|
|
29
|
+
};
|
|
30
|
+
interact_info?: {
|
|
31
|
+
liked?: boolean;
|
|
32
|
+
liked_count?: string;
|
|
33
|
+
comment_count?: string;
|
|
34
|
+
collected_count?: string;
|
|
35
|
+
shared_count?: string;
|
|
36
|
+
};
|
|
37
|
+
cover?: {
|
|
38
|
+
url_default?: string;
|
|
39
|
+
url_pre?: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
file_id?: string;
|
|
42
|
+
height?: number;
|
|
43
|
+
width?: number;
|
|
44
|
+
info_list?: Array<{
|
|
45
|
+
image_scene?: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
corner_tag_info?: Array<{
|
|
50
|
+
type?: string;
|
|
51
|
+
text?: string;
|
|
52
|
+
}>;
|
|
53
|
+
image_list?: Array<{
|
|
54
|
+
width?: number;
|
|
55
|
+
height?: number;
|
|
56
|
+
info_list?: Array<{
|
|
57
|
+
image_scene?: string;
|
|
58
|
+
url?: string;
|
|
59
|
+
}>;
|
|
60
|
+
}>;
|
|
61
|
+
video?: {
|
|
62
|
+
capa?: {
|
|
63
|
+
duration?: number;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type SearchCliValues = OutputCliValues;
|
|
70
|
+
|
|
71
|
+
export type SearchResult = {
|
|
72
|
+
ok: true;
|
|
73
|
+
search: {
|
|
74
|
+
keyword: string;
|
|
75
|
+
pageUrl: string;
|
|
76
|
+
fetchedAt: string;
|
|
77
|
+
total: number;
|
|
78
|
+
posts: RednotePost[];
|
|
79
|
+
savedPath?: string;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function parseSearchCliArgs(argv: string[]) {
|
|
84
|
+
return parseOutputCliArgs(argv, { includeKeyword: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function printSearchHelp() {
|
|
88
|
+
process.stdout.write(`rednote search
|
|
89
|
+
|
|
90
|
+
Usage:
|
|
91
|
+
npx -y @skills-store/rednote search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
|
|
92
|
+
node --experimental-strip-types ./scripts/rednote/search.ts --instance NAME --keyword TEXT [--format md|json] [--save [PATH]]
|
|
93
|
+
bun ./scripts/rednote/search.ts --instance NAME --keyword TEXT [--format md|json] [--save [PATH]]
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
--instance NAME Optional. Defaults to the saved lastConnect instance
|
|
97
|
+
--keyword TEXT Required. Search keyword
|
|
98
|
+
--format FORMAT Output format: md | json. Default: md
|
|
99
|
+
--save [PATH] Save posts as JSONL. Uses a default path when PATH is omitted
|
|
100
|
+
-h, --help Show this help
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeSearchPost(item: XHSSearchItem): RednotePost {
|
|
105
|
+
const noteCard = item.note_card ?? {};
|
|
106
|
+
const user = noteCard.user ?? {};
|
|
107
|
+
const interactInfo = noteCard.interact_info ?? {};
|
|
108
|
+
const cover = noteCard.cover ?? {};
|
|
109
|
+
const imageList = Array.isArray(noteCard.image_list) ? noteCard.image_list : [];
|
|
110
|
+
const cornerTagInfo = Array.isArray(noteCard.corner_tag_info) ? noteCard.corner_tag_info : [];
|
|
111
|
+
const xsecToken = item.xsec_token ?? null;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
id: item.id,
|
|
115
|
+
modelType: item.model_type,
|
|
116
|
+
xsecToken,
|
|
117
|
+
url: xsecToken
|
|
118
|
+
? `https://www.xiaohongshu.com/explore/${item.id}?xsec_token=${xsecToken}`
|
|
119
|
+
: `https://www.xiaohongshu.com/explore/${item.id}`,
|
|
120
|
+
noteCard: {
|
|
121
|
+
type: noteCard.type ?? null,
|
|
122
|
+
displayTitle: noteCard.display_title ?? null,
|
|
123
|
+
cover: {
|
|
124
|
+
urlDefault: cover.url_default ?? null,
|
|
125
|
+
urlPre: cover.url_pre ?? null,
|
|
126
|
+
url: cover.url ?? null,
|
|
127
|
+
fileId: cover.file_id ?? null,
|
|
128
|
+
width: cover.width ?? null,
|
|
129
|
+
height: cover.height ?? null,
|
|
130
|
+
infoList: Array.isArray(cover.info_list)
|
|
131
|
+
? cover.info_list.map((info) => ({
|
|
132
|
+
imageScene: info?.image_scene ?? null,
|
|
133
|
+
url: info?.url ?? null,
|
|
134
|
+
}))
|
|
135
|
+
: [],
|
|
136
|
+
},
|
|
137
|
+
user: {
|
|
138
|
+
userId: user.user_id ?? null,
|
|
139
|
+
nickname: user.nickname ?? null,
|
|
140
|
+
nickName: user.nick_name ?? user.nickname ?? null,
|
|
141
|
+
avatar: user.avatar ?? null,
|
|
142
|
+
xsecToken: user.xsec_token ?? null,
|
|
143
|
+
},
|
|
144
|
+
interactInfo: {
|
|
145
|
+
liked: interactInfo.liked ?? false,
|
|
146
|
+
likedCount: interactInfo.liked_count ?? null,
|
|
147
|
+
commentCount: interactInfo.comment_count ?? null,
|
|
148
|
+
collectedCount: interactInfo.collected_count ?? null,
|
|
149
|
+
sharedCount: interactInfo.shared_count ?? null,
|
|
150
|
+
},
|
|
151
|
+
cornerTagInfo: cornerTagInfo.map((tag) => ({
|
|
152
|
+
type: tag?.type ?? null,
|
|
153
|
+
text: tag?.text ?? null,
|
|
154
|
+
})),
|
|
155
|
+
imageList: imageList.map((image) => ({
|
|
156
|
+
width: image?.width ?? null,
|
|
157
|
+
height: image?.height ?? null,
|
|
158
|
+
infoList: Array.isArray(image?.info_list)
|
|
159
|
+
? image.info_list.map((info) => ({
|
|
160
|
+
imageScene: info?.image_scene ?? null,
|
|
161
|
+
url: info?.url ?? null,
|
|
162
|
+
}))
|
|
163
|
+
: [],
|
|
164
|
+
})),
|
|
165
|
+
video: {
|
|
166
|
+
duration: noteCard.video?.capa?.duration ?? null,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function getOrCreateXiaohongshuPage(session: RednoteSession) {
|
|
173
|
+
return session.page;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isJsonContentType(contentType: string | undefined) {
|
|
177
|
+
return typeof contentType === 'string' && contentType.includes('/json');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function collectSearchItems(page: Page, keyword: string) {
|
|
181
|
+
const items = new Map<string, XHSSearchItem>();
|
|
182
|
+
|
|
183
|
+
const searchPromise = new Promise<XHSSearchItem[]>((resolve, reject) => {
|
|
184
|
+
const handleResponse = async (response: Response) => {
|
|
185
|
+
try {
|
|
186
|
+
if (response.status() !== 200) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (response.request().method().toLowerCase() !== 'post') {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!isJsonContentType(response.headers()['content-type'])) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const data = await response.json() as { success?: boolean; data?: { items?: XHSSearchItem[] } };
|
|
199
|
+
const list = Array.isArray(data?.data?.items) ? data.data.items : null;
|
|
200
|
+
if (!data?.success || !list) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const item of list) {
|
|
205
|
+
if (item && item.model_type === 'note' && typeof item.id === 'string') {
|
|
206
|
+
items.set(item.id, item);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (items.size > 0) {
|
|
211
|
+
clearTimeout(timeoutId);
|
|
212
|
+
page.off('response', handleResponse);
|
|
213
|
+
resolve([...items.values()]);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const timeoutId = setTimeout(() => {
|
|
220
|
+
page.off('response', handleResponse);
|
|
221
|
+
reject(new Error(`Timed out waiting for Xiaohongshu search response: ${keyword}`));
|
|
222
|
+
}, 15_000);
|
|
223
|
+
|
|
224
|
+
page.on('response', handleResponse);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!page.url().startsWith('https://www.xiaohongshu.com/explore')) {
|
|
228
|
+
await page.goto('https://www.xiaohongshu.com/explore', {
|
|
229
|
+
waitUntil: 'domcontentloaded',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const searchInput = page.locator('#search-input');
|
|
234
|
+
await searchInput.focus();
|
|
235
|
+
await searchInput.fill(keyword);
|
|
236
|
+
await page.keyboard.press('Enter');
|
|
237
|
+
await page.waitForTimeout(500);
|
|
238
|
+
|
|
239
|
+
return await searchPromise;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function searchRednotePosts(session: RednoteSession, keyword: string): Promise<SearchResult> {
|
|
243
|
+
const page = await getOrCreateXiaohongshuPage(session);
|
|
244
|
+
const items = await collectSearchItems(page, keyword);
|
|
245
|
+
const posts = items.map(normalizeSearchPost);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
ok: true,
|
|
249
|
+
search: {
|
|
250
|
+
keyword,
|
|
251
|
+
pageUrl: page.url(),
|
|
252
|
+
fetchedAt: new Date().toISOString(),
|
|
253
|
+
total: posts.length,
|
|
254
|
+
posts,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function writeSearchOutput(result: SearchResult, values: SearchCliValues) {
|
|
260
|
+
const posts = result.search.posts;
|
|
261
|
+
let savedPath: string | undefined;
|
|
262
|
+
|
|
263
|
+
if (values.saveRequested) {
|
|
264
|
+
savedPath = resolveSavePath('search', values.savePath, result.search.keyword);
|
|
265
|
+
writePostsJsonl(posts, savedPath);
|
|
266
|
+
result.search.savedPath = savedPath;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (values.format === 'json') {
|
|
270
|
+
printJson(result);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let markdown = renderPostsMarkdown(posts);
|
|
275
|
+
if (savedPath) {
|
|
276
|
+
markdown = `Saved JSONL: ${savedPath}\n\n${markdown}`;
|
|
277
|
+
}
|
|
278
|
+
process.stdout.write(markdown);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function runSearchCommand(values: SearchCliValues = { format: 'md', saveRequested: false }) {
|
|
282
|
+
if (values.help) {
|
|
283
|
+
printSearchHelp();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
const keyword = values.keyword?.trim();
|
|
289
|
+
if (!keyword) {
|
|
290
|
+
throw new Error('Missing required option: --keyword');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const target = resolveStatusTarget(values.instance);
|
|
294
|
+
const session = await createRednoteSession(target);
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
await ensureRednoteLoggedIn(target, 'search', session);
|
|
298
|
+
const result = await searchRednotePosts(session, keyword);
|
|
299
|
+
writeSearchOutput(result, values);
|
|
300
|
+
} finally {
|
|
301
|
+
disconnectRednoteSession(session);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function main() {
|
|
306
|
+
const values = parseSearchCliArgs(process.argv.slice(2));
|
|
307
|
+
|
|
308
|
+
if (values.help) {
|
|
309
|
+
printSearchHelp();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await runSearchCommand(values);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
runCli(import.meta.url, main);
|