@skills-store/rednote 0.1.0 → 0.1.2
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/bin/rednote.js +16 -24
- package/dist/browser/connect-browser.js +172 -0
- package/dist/browser/create-browser.js +52 -0
- package/dist/browser/index.js +35 -0
- package/dist/browser/list-browser.js +50 -0
- package/dist/browser/remove-browser.js +69 -0
- package/{scripts/index.ts → dist/index.js} +19 -25
- package/dist/rednote/checkLogin.js +139 -0
- package/dist/rednote/env.js +69 -0
- package/dist/rednote/getFeedDetail.js +268 -0
- package/dist/rednote/getProfile.js +327 -0
- package/dist/rednote/home.js +210 -0
- package/dist/rednote/index.js +130 -0
- package/dist/rednote/login.js +109 -0
- package/dist/rednote/output-format.js +116 -0
- package/dist/rednote/publish.js +376 -0
- package/dist/rednote/search.js +207 -0
- package/dist/rednote/status.js +201 -0
- package/dist/utils/browser-cli.js +155 -0
- package/dist/utils/browser-core.js +705 -0
- package/package.json +7 -4
- package/scripts/browser/connect-browser.ts +0 -218
- package/scripts/browser/create-browser.ts +0 -81
- package/scripts/browser/index.ts +0 -49
- package/scripts/browser/list-browser.ts +0 -74
- package/scripts/browser/remove-browser.ts +0 -109
- package/scripts/rednote/checkLogin.ts +0 -171
- package/scripts/rednote/env.ts +0 -79
- package/scripts/rednote/getFeedDetail.ts +0 -351
- package/scripts/rednote/getProfile.ts +0 -420
- package/scripts/rednote/home.ts +0 -316
- package/scripts/rednote/index.ts +0 -122
- package/scripts/rednote/login.ts +0 -142
- package/scripts/rednote/output-format.ts +0 -156
- package/scripts/rednote/post-types.ts +0 -51
- package/scripts/rednote/search.ts +0 -316
- package/scripts/rednote/status.ts +0 -280
- package/scripts/utils/browser-cli.ts +0 -176
- package/scripts/utils/browser-core.ts +0 -906
- package/tsconfig.json +0 -13
- /package/{scripts/rednote/collect.ts → dist/rednote/collect.js} +0 -0
- /package/{scripts/rednote/publish.ts → dist/rednote/post-types.js} +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { printJson, runCli } from '../utils/browser-cli.js';
|
|
4
|
+
import { resolveStatusTarget } from './status.js';
|
|
5
|
+
import * as cheerio from 'cheerio';
|
|
6
|
+
import vm from 'node:vm';
|
|
7
|
+
import { parseOutputCliArgs, renderPostsMarkdown, resolveSavePath, writePostsJsonl } from './output-format.js';
|
|
8
|
+
import { createRednoteSession, disconnectRednoteSession } from './checkLogin.js';
|
|
9
|
+
export function parseHomeCliArgs(argv) {
|
|
10
|
+
return parseOutputCliArgs(argv);
|
|
11
|
+
}
|
|
12
|
+
function printHomeHelp() {
|
|
13
|
+
process.stdout.write(`rednote home
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npx -y @skills-store/rednote home [--instance NAME] [--format md|json] [--save [PATH]]
|
|
17
|
+
node --experimental-strip-types ./scripts/rednote/home.ts --instance NAME [--format md|json] [--save [PATH]]
|
|
18
|
+
bun ./scripts/rednote/home.ts --instance NAME [--format md|json] [--save [PATH]]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--instance NAME Optional. Defaults to the saved lastConnect instance
|
|
22
|
+
--format FORMAT Output format: md | json. Default: md
|
|
23
|
+
--save [PATH] Save posts as JSONL. Uses a default path when PATH is omitted
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
function normalizeHomePost(item) {
|
|
28
|
+
const noteCard = item.noteCard ?? {};
|
|
29
|
+
const user = noteCard.user ?? {};
|
|
30
|
+
const interactInfo = noteCard.interactInfo ?? {};
|
|
31
|
+
const cover = noteCard.cover ?? {};
|
|
32
|
+
const imageList = Array.isArray(noteCard.imageList) ? noteCard.imageList : [];
|
|
33
|
+
const cornerTagInfo = Array.isArray(noteCard.cornerTagInfo) ? noteCard.cornerTagInfo : [];
|
|
34
|
+
const xsecToken = item.xsecToken ?? null;
|
|
35
|
+
return {
|
|
36
|
+
id: item.id,
|
|
37
|
+
modelType: item.modelType,
|
|
38
|
+
xsecToken,
|
|
39
|
+
url: xsecToken ? `https://www.xiaohongshu.com/explore/${item.id}?xsec_token=${xsecToken}` : `https://www.xiaohongshu.com/explore/${item.id}`,
|
|
40
|
+
noteCard: {
|
|
41
|
+
type: noteCard.type ?? null,
|
|
42
|
+
displayTitle: noteCard.displayTitle ?? null,
|
|
43
|
+
cover: {
|
|
44
|
+
urlDefault: cover.urlDefault ?? null,
|
|
45
|
+
urlPre: cover.urlPre ?? null,
|
|
46
|
+
url: cover.url ?? null,
|
|
47
|
+
fileId: cover.fileId ?? null,
|
|
48
|
+
width: cover.width ?? null,
|
|
49
|
+
height: cover.height ?? null,
|
|
50
|
+
infoList: Array.isArray(cover.infoList) ? cover.infoList.map((info)=>({
|
|
51
|
+
imageScene: info?.imageScene ?? null,
|
|
52
|
+
url: info?.url ?? null
|
|
53
|
+
})) : []
|
|
54
|
+
},
|
|
55
|
+
user: {
|
|
56
|
+
userId: user.userId ?? null,
|
|
57
|
+
nickname: user.nickname ?? null,
|
|
58
|
+
nickName: user.nickName ?? user.nickname ?? null,
|
|
59
|
+
avatar: user.avatar ?? null,
|
|
60
|
+
xsecToken: user.xsecToken ?? null
|
|
61
|
+
},
|
|
62
|
+
interactInfo: {
|
|
63
|
+
liked: interactInfo.liked ?? false,
|
|
64
|
+
likedCount: interactInfo.likedCount ?? null,
|
|
65
|
+
commentCount: interactInfo.commentCount ?? null,
|
|
66
|
+
collectedCount: interactInfo.collectedCount ?? null,
|
|
67
|
+
sharedCount: interactInfo.sharedCount ?? null
|
|
68
|
+
},
|
|
69
|
+
cornerTagInfo: cornerTagInfo.map((tag)=>({
|
|
70
|
+
type: tag?.type ?? null,
|
|
71
|
+
text: tag?.text ?? null
|
|
72
|
+
})),
|
|
73
|
+
imageList: imageList.map((image)=>({
|
|
74
|
+
width: image?.width ?? null,
|
|
75
|
+
height: image?.height ?? null,
|
|
76
|
+
infoList: Array.isArray(image?.infoList) ? image.infoList.map((info)=>({
|
|
77
|
+
imageScene: info?.imageScene ?? null,
|
|
78
|
+
url: info?.url ?? null
|
|
79
|
+
})) : []
|
|
80
|
+
})),
|
|
81
|
+
video: {
|
|
82
|
+
duration: noteCard.video?.capa?.duration ?? null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function getOrCreateXiaohongshuPage(session) {
|
|
88
|
+
return session.page;
|
|
89
|
+
}
|
|
90
|
+
async function collectHomeFeedItems(page) {
|
|
91
|
+
const items = new Map();
|
|
92
|
+
const feedPromise = new Promise((resolve, reject)=>{
|
|
93
|
+
const handleResponse = async (response)=>{
|
|
94
|
+
try {
|
|
95
|
+
if (response.status() !== 200) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (response.request().method().toLowerCase() !== 'get') {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const url = new URL(response.url());
|
|
102
|
+
if (!url.href.endsWith('/explore')) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const html = await response.text();
|
|
106
|
+
const $ = cheerio.load(html);
|
|
107
|
+
$('script').each((_, element)=>{
|
|
108
|
+
const scriptContent = $(element).html();
|
|
109
|
+
if (!scriptContent?.includes('window.__INITIAL_STATE__')) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const scriptText = scriptContent.substring(scriptContent.indexOf('=') + 1).trim();
|
|
113
|
+
const sandbox = {};
|
|
114
|
+
vm.createContext(sandbox);
|
|
115
|
+
vm.runInContext(`var info = ${scriptText}`, sandbox);
|
|
116
|
+
const feeds = sandbox.info?.feed?.feeds;
|
|
117
|
+
if (!Array.isArray(feeds)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
for (const feed of feeds){
|
|
121
|
+
if (feed && feed.modelType === 'note' && typeof feed.id === 'string') {
|
|
122
|
+
items.set(feed.id, feed);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
if (items.size > 0) {
|
|
127
|
+
clearTimeout(timeoutId);
|
|
128
|
+
page.off('response', handleResponse);
|
|
129
|
+
resolve([
|
|
130
|
+
...items.values()
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
};
|
|
135
|
+
const timeoutId = setTimeout(()=>{
|
|
136
|
+
page.off('response', handleResponse);
|
|
137
|
+
reject(new Error('Timed out waiting for Xiaohongshu home feed response'));
|
|
138
|
+
}, 15_000);
|
|
139
|
+
page.on('response', handleResponse);
|
|
140
|
+
});
|
|
141
|
+
if (page.url().startsWith('https://www.xiaohongshu.com/explore')) {
|
|
142
|
+
await page.reload({
|
|
143
|
+
waitUntil: 'domcontentloaded'
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
await page.goto('https://www.xiaohongshu.com/explore', {
|
|
147
|
+
waitUntil: 'domcontentloaded'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
await page.waitForTimeout(500);
|
|
151
|
+
return await feedPromise;
|
|
152
|
+
}
|
|
153
|
+
export async function getRednoteHomePosts(session) {
|
|
154
|
+
const page = await getOrCreateXiaohongshuPage(session);
|
|
155
|
+
const items = await collectHomeFeedItems(page);
|
|
156
|
+
const posts = items.map(normalizeHomePost);
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
home: {
|
|
160
|
+
pageUrl: page.url(),
|
|
161
|
+
fetchedAt: new Date().toISOString(),
|
|
162
|
+
total: posts.length,
|
|
163
|
+
posts
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function writeHomeOutput(result, values) {
|
|
168
|
+
const posts = result.home.posts;
|
|
169
|
+
let savedPath;
|
|
170
|
+
if (values.saveRequested) {
|
|
171
|
+
savedPath = resolveSavePath('home', values.savePath);
|
|
172
|
+
writePostsJsonl(posts, savedPath);
|
|
173
|
+
result.home.savedPath = savedPath;
|
|
174
|
+
}
|
|
175
|
+
if (values.format === 'json') {
|
|
176
|
+
printJson(result);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let markdown = renderPostsMarkdown(posts);
|
|
180
|
+
if (savedPath) {
|
|
181
|
+
markdown = `Saved JSONL: ${savedPath}\n\n${markdown}`;
|
|
182
|
+
}
|
|
183
|
+
process.stdout.write(markdown);
|
|
184
|
+
}
|
|
185
|
+
export async function runHomeCommand(values = {
|
|
186
|
+
format: 'md',
|
|
187
|
+
saveRequested: false
|
|
188
|
+
}) {
|
|
189
|
+
if (values.help) {
|
|
190
|
+
printHomeHelp();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const target = resolveStatusTarget(values.instance);
|
|
194
|
+
const session = await createRednoteSession(target);
|
|
195
|
+
try {
|
|
196
|
+
const result = await getRednoteHomePosts(session);
|
|
197
|
+
writeHomeOutput(result, values);
|
|
198
|
+
} finally{
|
|
199
|
+
disconnectRednoteSession(session);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function main() {
|
|
203
|
+
const values = parseHomeCliArgs(process.argv.slice(2));
|
|
204
|
+
if (values.help) {
|
|
205
|
+
printHomeHelp();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
await runHomeCommand(values);
|
|
209
|
+
}
|
|
210
|
+
runCli(import.meta.url, main);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { runCli } from '../utils/browser-cli.js';
|
|
4
|
+
function printRednoteHelp() {
|
|
5
|
+
process.stdout.write(`rednote
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
browser <list|create|remove|connect>
|
|
9
|
+
env [--format md|json]
|
|
10
|
+
status [--instance NAME]
|
|
11
|
+
check-login [--instance NAME]
|
|
12
|
+
login [--instance NAME]
|
|
13
|
+
publish [--instance NAME]
|
|
14
|
+
home [--instance NAME] [--format md|json] [--save [PATH]]
|
|
15
|
+
search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
|
|
16
|
+
get-feed-detail [--instance NAME] --url URL [--url URL] [--format md|json]
|
|
17
|
+
get-profile [--instance NAME] --id USER_ID [--format md|json]
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
npx -y @skills-store/rednote browser list
|
|
21
|
+
npx -y @skills-store/rednote browser create --name seller-main --browser chrome
|
|
22
|
+
npx -y @skills-store/rednote env
|
|
23
|
+
npx -y @skills-store/rednote status --instance seller-main
|
|
24
|
+
npx -y @skills-store/rednote login --instance seller-main
|
|
25
|
+
npx -y @skills-store/rednote publish --instance seller-main --type video --video ./note.mp4 --title 标题 --content 描述
|
|
26
|
+
npx -y @skills-store/rednote home --instance seller-main --format md --save
|
|
27
|
+
npx -y @skills-store/rednote search --instance seller-main --keyword 护肤 --format json --save ./output/search.jsonl
|
|
28
|
+
npx -y @skills-store/rednote get-feed-detail --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy"
|
|
29
|
+
npx -y @skills-store/rednote get-profile --instance seller-main --id USER_ID
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
function parseBasicArgs(argv) {
|
|
33
|
+
const { values } = parseArgs({
|
|
34
|
+
args: argv,
|
|
35
|
+
allowPositionals: true,
|
|
36
|
+
strict: false,
|
|
37
|
+
options: {
|
|
38
|
+
instance: {
|
|
39
|
+
type: 'string'
|
|
40
|
+
},
|
|
41
|
+
keyword: {
|
|
42
|
+
type: 'string'
|
|
43
|
+
},
|
|
44
|
+
format: {
|
|
45
|
+
type: 'string'
|
|
46
|
+
},
|
|
47
|
+
help: {
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
short: 'h'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
export async function runRednoteCli(argv = process.argv.slice(2)) {
|
|
56
|
+
const rawArgv = argv;
|
|
57
|
+
const firstArg = rawArgv[0];
|
|
58
|
+
if (!firstArg || firstArg === 'help' || firstArg === '--help' || firstArg === '-h') {
|
|
59
|
+
printRednoteHelp();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const command = !firstArg.startsWith('-') ? firstArg : 'status';
|
|
63
|
+
const commandArgv = firstArg === command ? rawArgv.slice(1) : rawArgv;
|
|
64
|
+
const basicValues = parseBasicArgs(commandArgv);
|
|
65
|
+
if (command === 'env') {
|
|
66
|
+
const { runEnvCommand } = await import('./env.js');
|
|
67
|
+
await runEnvCommand({
|
|
68
|
+
format: basicValues.format === 'json' ? 'json' : 'md',
|
|
69
|
+
help: basicValues.help
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (command === 'status') {
|
|
74
|
+
const { runStatusCommand } = await import('./status.js');
|
|
75
|
+
await runStatusCommand({
|
|
76
|
+
instance: basicValues.instance,
|
|
77
|
+
help: basicValues.help
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (command === 'check-login') {
|
|
82
|
+
const { runCheckLoginCommand } = await import('./checkLogin.js');
|
|
83
|
+
await runCheckLoginCommand({
|
|
84
|
+
instance: basicValues.instance,
|
|
85
|
+
help: basicValues.help
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (command === 'login') {
|
|
90
|
+
const { runLoginCommand } = await import('./login.js');
|
|
91
|
+
await runLoginCommand({
|
|
92
|
+
instance: basicValues.instance,
|
|
93
|
+
help: basicValues.help
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (command === 'publish') {
|
|
98
|
+
const { runPublishCommand } = await import('./publish.js');
|
|
99
|
+
await runPublishCommand({
|
|
100
|
+
instance: basicValues.instance,
|
|
101
|
+
help: basicValues.help
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (command === 'home') {
|
|
106
|
+
const { parseHomeCliArgs, runHomeCommand } = await import('./home.js');
|
|
107
|
+
await runHomeCommand(parseHomeCliArgs(commandArgv));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (command === 'search') {
|
|
111
|
+
const { parseSearchCliArgs, runSearchCommand } = await import('./search.js');
|
|
112
|
+
await runSearchCommand(parseSearchCliArgs(commandArgv));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (command === 'get-feed-detail') {
|
|
116
|
+
const { parseGetFeedDetailCliArgs, runGetFeedDetailCommand } = await import('./getFeedDetail.js');
|
|
117
|
+
await runGetFeedDetailCommand(parseGetFeedDetailCliArgs(commandArgv));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (command === 'get-profile') {
|
|
121
|
+
const { parseGetProfileCliArgs, runGetProfileCommand } = await import('./getProfile.js');
|
|
122
|
+
await runGetProfileCommand(parseGetProfileCliArgs(commandArgv));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Unknown command: ${command}`);
|
|
126
|
+
}
|
|
127
|
+
async function main() {
|
|
128
|
+
await runRednoteCli();
|
|
129
|
+
}
|
|
130
|
+
runCli(import.meta.url, main);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { printJson, runCli } from '../utils/browser-cli.js';
|
|
4
|
+
import { resolveStatusTarget } from './status.js';
|
|
5
|
+
import { createRednoteSession, disconnectRednoteSession } from './checkLogin.js';
|
|
6
|
+
function printLoginHelp() {
|
|
7
|
+
process.stdout.write(`rednote login
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
npx -y @skills-store/rednote login [--instance NAME]
|
|
11
|
+
node --experimental-strip-types ./scripts/rednote/login.ts --instance NAME
|
|
12
|
+
bun ./scripts/rednote/login.ts --instance NAME
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--instance NAME Optional. Defaults to the saved lastConnect instance
|
|
16
|
+
-h, --help Show this help
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
async function getOrCreateXiaohongshuPage(session) {
|
|
20
|
+
const page = session.page;
|
|
21
|
+
if (!page.url().startsWith('https://www.xiaohongshu.com/')) {
|
|
22
|
+
await page.goto('https://www.xiaohongshu.com/explore', {
|
|
23
|
+
waitUntil: 'domcontentloaded'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
page
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function openRednoteLogin(target, session) {
|
|
31
|
+
const { page } = await getOrCreateXiaohongshuPage(session);
|
|
32
|
+
await page.waitForTimeout(2_000);
|
|
33
|
+
const loginButton = page.locator('#login-btn');
|
|
34
|
+
const hasLoginButton = await loginButton.count() > 0;
|
|
35
|
+
if (!hasLoginButton) {
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
instance: {
|
|
39
|
+
scope: target.scope,
|
|
40
|
+
name: target.instanceName,
|
|
41
|
+
browser: target.browser,
|
|
42
|
+
userDataDir: target.userDataDir,
|
|
43
|
+
source: target.source,
|
|
44
|
+
lastConnect: target.lastConnect
|
|
45
|
+
},
|
|
46
|
+
rednote: {
|
|
47
|
+
loginClicked: false,
|
|
48
|
+
pageUrl: page.url(),
|
|
49
|
+
waitingForPhoneLogin: false,
|
|
50
|
+
message: '未检测到登录按钮,当前实例可能已经登录。'
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
await loginButton.first().click();
|
|
55
|
+
await page.waitForTimeout(500);
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
instance: {
|
|
59
|
+
scope: target.scope,
|
|
60
|
+
name: target.instanceName,
|
|
61
|
+
browser: target.browser,
|
|
62
|
+
userDataDir: target.userDataDir,
|
|
63
|
+
source: target.source,
|
|
64
|
+
lastConnect: target.lastConnect
|
|
65
|
+
},
|
|
66
|
+
rednote: {
|
|
67
|
+
loginClicked: true,
|
|
68
|
+
pageUrl: page.url(),
|
|
69
|
+
waitingForPhoneLogin: true,
|
|
70
|
+
message: '已点击登录按钮,请在浏览器中继续输入手机号并完成登录。'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function runLoginCommand(values = {}) {
|
|
75
|
+
if (values.help) {
|
|
76
|
+
printLoginHelp();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const target = resolveStatusTarget(values.instance);
|
|
80
|
+
const session = await createRednoteSession(target);
|
|
81
|
+
try {
|
|
82
|
+
const result = await openRednoteLogin(target, session);
|
|
83
|
+
printJson(result);
|
|
84
|
+
} finally{
|
|
85
|
+
disconnectRednoteSession(session);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function main() {
|
|
89
|
+
const { values } = parseArgs({
|
|
90
|
+
args: process.argv.slice(2),
|
|
91
|
+
allowPositionals: true,
|
|
92
|
+
strict: false,
|
|
93
|
+
options: {
|
|
94
|
+
instance: {
|
|
95
|
+
type: 'string'
|
|
96
|
+
},
|
|
97
|
+
help: {
|
|
98
|
+
type: 'boolean',
|
|
99
|
+
short: 'h'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
if (values.help) {
|
|
104
|
+
printLoginHelp();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await runLoginCommand(values);
|
|
108
|
+
}
|
|
109
|
+
runCli(import.meta.url, main);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const REDNOTE_ROOT = path.resolve(SCRIPT_DIR, '../..');
|
|
6
|
+
function parseOptionWithEquals(arg) {
|
|
7
|
+
const equalIndex = arg.indexOf('=');
|
|
8
|
+
if (equalIndex === -1) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
key: arg.slice(0, equalIndex),
|
|
13
|
+
value: arg.slice(equalIndex + 1)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function parseOutputCliArgs(argv, options = {}) {
|
|
17
|
+
const values = {
|
|
18
|
+
format: 'md',
|
|
19
|
+
saveRequested: false,
|
|
20
|
+
help: false
|
|
21
|
+
};
|
|
22
|
+
for(let index = 0; index < argv.length; index += 1){
|
|
23
|
+
const arg = argv[index];
|
|
24
|
+
const withEquals = parseOptionWithEquals(arg);
|
|
25
|
+
if (arg === '-h' || arg === '--help') {
|
|
26
|
+
values.help = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (withEquals?.key === '--instance') {
|
|
30
|
+
values.instance = withEquals.value;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg === '--instance') {
|
|
34
|
+
values.instance = argv[index + 1];
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (options.includeKeyword && withEquals?.key === '--keyword') {
|
|
39
|
+
values.keyword = withEquals.value;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (options.includeKeyword && arg === '--keyword') {
|
|
43
|
+
values.keyword = argv[index + 1];
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (withEquals?.key === '--format') {
|
|
48
|
+
const format = withEquals.value;
|
|
49
|
+
if (format !== 'json' && format !== 'md') {
|
|
50
|
+
throw new Error(`Invalid --format value: ${format}`);
|
|
51
|
+
}
|
|
52
|
+
values.format = format;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === '--format') {
|
|
56
|
+
const format = argv[index + 1];
|
|
57
|
+
if (format !== 'json' && format !== 'md') {
|
|
58
|
+
throw new Error(`Invalid --format value: ${String(format)}`);
|
|
59
|
+
}
|
|
60
|
+
values.format = format;
|
|
61
|
+
index += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (withEquals?.key === '--save') {
|
|
65
|
+
values.saveRequested = true;
|
|
66
|
+
values.savePath = withEquals.value;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === '--save') {
|
|
70
|
+
values.saveRequested = true;
|
|
71
|
+
const nextArg = argv[index + 1];
|
|
72
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
73
|
+
values.savePath = nextArg;
|
|
74
|
+
index += 1;
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return values;
|
|
80
|
+
}
|
|
81
|
+
function slugifyKeyword(keyword) {
|
|
82
|
+
return keyword.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\p{Letter}\p{Number}-]+/gu, '').slice(0, 32) || 'query';
|
|
83
|
+
}
|
|
84
|
+
function timestampForFilename() {
|
|
85
|
+
return new Date().toISOString().replaceAll(':', '').replaceAll('.', '').replace('T', '-').replace('Z', 'Z');
|
|
86
|
+
}
|
|
87
|
+
export function resolveSavePath(command, explicitPath, keyword) {
|
|
88
|
+
if (explicitPath) {
|
|
89
|
+
return path.resolve(explicitPath);
|
|
90
|
+
}
|
|
91
|
+
const keywordSuffix = keyword ? `-${slugifyKeyword(keyword)}` : '';
|
|
92
|
+
return path.join(REDNOTE_ROOT, 'output', `${command}${keywordSuffix}-${timestampForFilename()}.jsonl`);
|
|
93
|
+
}
|
|
94
|
+
export function writePostsJsonl(posts, filePath) {
|
|
95
|
+
fs.mkdirSync(path.dirname(filePath), {
|
|
96
|
+
recursive: true
|
|
97
|
+
});
|
|
98
|
+
const content = posts.map((post)=>JSON.stringify(post)).join('\n');
|
|
99
|
+
fs.writeFileSync(filePath, content ? `${content}\n` : '', 'utf8');
|
|
100
|
+
}
|
|
101
|
+
function formatField(value) {
|
|
102
|
+
return value ?? '';
|
|
103
|
+
}
|
|
104
|
+
export function renderPostsMarkdown(posts) {
|
|
105
|
+
if (posts.length === 0) {
|
|
106
|
+
return '没有获取到帖子。\n';
|
|
107
|
+
}
|
|
108
|
+
return `${posts.map((post)=>[
|
|
109
|
+
`- id: ${post.id}`,
|
|
110
|
+
`- displayTitle: ${formatField(post.noteCard.displayTitle)}`,
|
|
111
|
+
`- likedCount: ${formatField(post.noteCard.interactInfo.likedCount)}`,
|
|
112
|
+
`- url: ${post.url}`,
|
|
113
|
+
`- nickName: ${formatField(post.noteCard.user.nickName)}`,
|
|
114
|
+
`- userId: ${formatField(post.noteCard.user.userId)}`
|
|
115
|
+
].join('\n')).join('\n\n')}\n`;
|
|
116
|
+
}
|