@skills-store/rednote 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,21 +2,24 @@
2
2
  import * as cheerio from 'cheerio';
3
3
  import { parseArgs } from 'node:util';
4
4
  import vm from 'node:vm';
5
- import { printJson, runCli } from '../utils/browser-cli.js';
5
+ import { runCli } from '../utils/browser-cli.js';
6
6
  import { resolveStatusTarget } from './status.js';
7
7
  import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
8
+ import { ensureJsonSavePath, renderJsonSaveSummary, renderPostsMarkdown, resolveJsonSavePath, writeJsonFile } from './output-format.js';
8
9
  function printGetProfileHelp() {
9
10
  process.stdout.write(`rednote get-profile
10
11
 
11
12
  Usage:
12
- npx -y @skills-store/rednote get-profile [--instance NAME] --id USER_ID [--format md|json]
13
- node --experimental-strip-types ./scripts/rednote/getProfile.ts --instance NAME --id USER_ID [--format md|json]
14
- bun ./scripts/rednote/getProfile.ts --instance NAME --id USER_ID [--format md|json]
13
+ npx -y @skills-store/rednote get-profile [--instance NAME] --id USER_ID [--mode profile|notes] [--format md|json] [--save PATH]
14
+ node --experimental-strip-types ./scripts/rednote/getProfile.ts --instance NAME --id USER_ID [--mode profile|notes] [--format md|json] [--save PATH]
15
+ bun ./scripts/rednote/getProfile.ts --instance NAME --id USER_ID [--mode profile|notes] [--format md|json] [--save PATH]
15
16
 
16
17
  Options:
17
18
  --instance NAME Optional. Defaults to the saved lastConnect instance
18
19
  --id USER_ID Required. Xiaohongshu profile user id
20
+ --mode MODE Optional. profile | notes. Default: profile
19
21
  --format FORMAT Output format: md | json. Default: md
22
+ --save PATH Required when --format json is used. Saves only the selected mode data as JSON
20
23
  -h, --help Show this help
21
24
  `);
22
25
  }
@@ -35,6 +38,12 @@ export function parseGetProfileCliArgs(argv) {
35
38
  format: {
36
39
  type: 'string'
37
40
  },
41
+ mode: {
42
+ type: 'string'
43
+ },
44
+ save: {
45
+ type: 'string'
46
+ },
38
47
  help: {
39
48
  type: 'boolean',
40
49
  short: 'h'
@@ -48,10 +57,16 @@ export function parseGetProfileCliArgs(argv) {
48
57
  if (format !== 'md' && format !== 'json') {
49
58
  throw new Error(`Invalid --format value: ${String(format)}`);
50
59
  }
60
+ const mode = values.mode ?? 'profile';
61
+ if (mode !== 'profile' && mode !== 'notes') {
62
+ throw new Error(`Invalid --mode value: ${String(values.mode)}`);
63
+ }
51
64
  return {
52
65
  instance: values.instance,
53
66
  id: values.id,
54
67
  format,
68
+ mode,
69
+ savePath: values.save,
55
70
  help: values.help
56
71
  };
57
72
  }
@@ -103,8 +118,7 @@ function normalizeProfileUser(userPageData) {
103
118
  follows: follows,
104
119
  fans: fans,
105
120
  interaction: interaction,
106
- tags,
107
- raw: userPageData
121
+ tags
108
122
  };
109
123
  }
110
124
  function normalizeProfileNote(item) {
@@ -197,8 +211,8 @@ function normalizeProfileNotes(notesRaw) {
197
211
  function formatProfileField(value) {
198
212
  return value ?? '';
199
213
  }
200
- function renderProfileMarkdown(result) {
201
- const { user, notes, url, userId } = result.profile;
214
+ function renderProfileUserMarkdown(result) {
215
+ const { user, url, userId } = result.profile;
202
216
  const lines = [];
203
217
  lines.push('## UserInfo');
204
218
  lines.push('');
@@ -211,23 +225,17 @@ function renderProfileMarkdown(result) {
211
225
  lines.push(`- Fans: ${formatProfileField(user.fans)}`);
212
226
  lines.push(`- Interactions: ${formatProfileField(user.interaction)}`);
213
227
  lines.push(`- Tags: ${user.tags.length > 0 ? user.tags.map((tag)=>`#${tag}`).join(' ') : ''}`);
214
- lines.push('');
215
- lines.push('## Notes');
216
- lines.push('');
217
- if (notes.length === 0) {
218
- lines.push('- Notes not found or the profile is private');
219
- } else {
220
- notes.forEach((note, index)=>{
221
- lines.push(`- Title: ${note.noteCard.displayTitle ?? ''}`);
222
- lines.push(` Url: ${note.url}`);
223
- lines.push(` Interaction: ${note.noteCard.interactInfo.likedCount ?? ''}`);
224
- if (index < notes.length - 1) {
225
- lines.push('');
226
- }
227
- });
228
- }
229
228
  return `${lines.join('\n')}\n`;
230
229
  }
230
+ function selectProfileOutput(result, mode) {
231
+ return mode === 'notes' ? result.profile.notes : result.profile.user;
232
+ }
233
+ function renderProfileMarkdown(result, mode) {
234
+ if (mode === 'notes') {
235
+ return renderPostsMarkdown(result.profile.notes);
236
+ }
237
+ return renderProfileUserMarkdown(result);
238
+ }
231
239
  async function captureProfile(page, targetUrl) {
232
240
  let userPageData = null;
233
241
  let notes = null;
@@ -286,26 +294,33 @@ export async function getProfile(session, url, userId) {
286
294
  userId,
287
295
  url,
288
296
  fetchedAt: new Date().toISOString(),
289
- user: normalizeProfileUser(captured.userPageData),
290
- notes: normalizeProfileNotes(captured.notes),
291
- raw: captured
297
+ user: normalizeProfileUser({
298
+ ...captured.userPageData,
299
+ userId
300
+ }),
301
+ notes: normalizeProfileNotes(captured.notes)
292
302
  }
293
303
  };
294
304
  }
295
- function writeProfileOutput(result, format) {
296
- if (format === 'json') {
297
- printJson(result);
305
+ function writeProfileOutput(result, values) {
306
+ const output = selectProfileOutput(result, values.mode);
307
+ if (values.format === 'json') {
308
+ const savedPath = resolveJsonSavePath(values.savePath);
309
+ writeJsonFile(output, savedPath);
310
+ process.stdout.write(renderJsonSaveSummary(savedPath, output));
298
311
  return;
299
312
  }
300
- process.stdout.write(renderProfileMarkdown(result));
313
+ process.stdout.write(renderProfileMarkdown(result, values.mode));
301
314
  }
302
315
  export async function runGetProfileCommand(values = {
303
- format: 'md'
316
+ format: 'md',
317
+ mode: 'profile'
304
318
  }) {
305
319
  if (values.help) {
306
320
  printGetProfileHelp();
307
321
  return;
308
322
  }
323
+ ensureJsonSavePath(values.format, values.savePath);
309
324
  if (!values.id) {
310
325
  throw new Error('Missing required option: --id');
311
326
  }
@@ -315,7 +330,7 @@ export async function runGetProfileCommand(values = {
315
330
  try {
316
331
  await ensureRednoteLoggedIn(target, 'fetching profile', session);
317
332
  const result = await getProfile(session, buildProfileUrl(normalizedUserId), normalizedUserId);
318
- writeProfileOutput(result, values.format);
333
+ writeProfileOutput(result, values);
319
334
  } finally{
320
335
  await disconnectRednoteSession(session);
321
336
  }
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
- import { printJson, runCli } from '../utils/browser-cli.js';
3
+ import { runCli } from '../utils/browser-cli.js';
4
4
  import { resolveStatusTarget } from './status.js';
5
5
  import * as cheerio from 'cheerio';
6
6
  import vm from 'node:vm';
7
- import { parseOutputCliArgs, renderPostsMarkdown, resolveSavePath, writePostsJsonl } from './output-format.js';
7
+ import { ensureJsonSavePath, parseOutputCliArgs, renderJsonSaveSummary, renderPostsMarkdown, resolveJsonSavePath, resolveSavePath, writeJsonFile, writePostsJsonl } from './output-format.js';
8
8
  import { createRednoteSession, disconnectRednoteSession } from './checkLogin.js';
9
9
  export function parseHomeCliArgs(argv) {
10
10
  return parseOutputCliArgs(argv);
@@ -20,7 +20,7 @@ Usage:
20
20
  Options:
21
21
  --instance NAME Optional. Defaults to the saved lastConnect instance
22
22
  --format FORMAT Output format: md | json. Default: md
23
- --save [PATH] Save posts as JSONL. Uses a default path when PATH is omitted
23
+ --save [PATH] In markdown mode, saves posts as JSONL and uses a default path when PATH is omitted. In json mode, PATH is required and the full result is saved as JSON
24
24
  -h, --help Show this help
25
25
  `);
26
26
  }
@@ -165,6 +165,13 @@ export async function getRednoteHomePosts(session) {
165
165
  };
166
166
  }
167
167
  function writeHomeOutput(result, values) {
168
+ if (values.format === 'json') {
169
+ const savedPath = resolveJsonSavePath(values.savePath);
170
+ result.home.savedPath = savedPath;
171
+ writeJsonFile(result.home.posts, savedPath);
172
+ process.stdout.write(renderJsonSaveSummary(savedPath, result.home.posts));
173
+ return;
174
+ }
168
175
  const posts = result.home.posts;
169
176
  let savedPath;
170
177
  if (values.saveRequested) {
@@ -172,10 +179,6 @@ function writeHomeOutput(result, values) {
172
179
  writePostsJsonl(posts, savedPath);
173
180
  result.home.savedPath = savedPath;
174
181
  }
175
- if (values.format === 'json') {
176
- printJson(result);
177
- return;
178
- }
179
182
  let markdown = renderPostsMarkdown(posts);
180
183
  if (savedPath) {
181
184
  markdown = `Saved JSONL: ${savedPath}\n\n${markdown}`;
@@ -190,6 +193,7 @@ export async function runHomeCommand(values = {
190
193
  printHomeHelp();
191
194
  return;
192
195
  }
196
+ ensureJsonSavePath(values.format, values.savePath);
193
197
  const target = resolveStatusTarget(values.instance);
194
198
  const session = await createRednoteSession(target);
195
199
  try {
@@ -6,7 +6,7 @@ function printRednoteHelp() {
6
6
 
7
7
  Commands:
8
8
  browser <list|create|remove|connect>
9
- env [--format md|json]
9
+ env [--format md|json] [--save PATH]
10
10
  status [--instance NAME]
11
11
  check-login [--instance NAME]
12
12
  login [--instance NAME]
@@ -14,21 +14,22 @@ Commands:
14
14
  interact [--instance NAME] --url URL [--like] [--collect] [--comment TEXT]
15
15
  home [--instance NAME] [--format md|json] [--save [PATH]]
16
16
  search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
17
- get-feed-detail [--instance NAME] --url URL [--url URL] [--format md|json]
18
- get-profile [--instance NAME] --id USER_ID [--format md|json]
17
+ get-feed-detail [--instance NAME] --url URL [--url URL] [--comments [COUNT]] [--format md|json] [--save PATH]
18
+ get-profile [--instance NAME] --id USER_ID [--mode profile|notes] [--format md|json] [--save PATH]
19
19
 
20
20
  Examples:
21
21
  npx -y @skills-store/rednote browser list
22
22
  npx -y @skills-store/rednote browser create --name seller-main --browser chrome
23
23
  npx -y @skills-store/rednote env
24
+ npx -y @skills-store/rednote env --format json --save ./output/env.json
24
25
  npx -y @skills-store/rednote status --instance seller-main
25
26
  npx -y @skills-store/rednote login --instance seller-main
26
- npx -y @skills-store/rednote publish --instance seller-main --type video --video ./note.mp4 --title 标题 --content 描述
27
- npx -y @skills-store/rednote interact --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect --comment "写得真好"
27
+ npx -y @skills-store/rednote publish --instance seller-main --type video --video ./note.mp4 --title "Video title" --content "Video description"
28
+ npx -y @skills-store/rednote interact --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --like --collect --comment "Great post"
28
29
  npx -y @skills-store/rednote home --instance seller-main --format md --save
29
- npx -y @skills-store/rednote search --instance seller-main --keyword 护肤 --format json --save ./output/search.jsonl
30
- npx -y @skills-store/rednote get-feed-detail --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy"
31
- npx -y @skills-store/rednote get-profile --instance seller-main --id USER_ID
30
+ npx -y @skills-store/rednote search --instance seller-main --keyword skincare --format json --save ./output/search.json
31
+ npx -y @skills-store/rednote get-feed-detail --instance seller-main --url "https://www.xiaohongshu.com/explore/xxx?xsec_token=yyy" --comments 100 --format json --save ./output/feed-detail.json
32
+ npx -y @skills-store/rednote get-profile --instance seller-main --id USER_ID --mode notes --format json --save ./output/profile-notes.json
32
33
  `);
33
34
  }
34
35
  function parseBasicArgs(argv) {
@@ -135,7 +135,7 @@ async function requireVisibleLocator(locator, errorMessage, timeoutMs = 5_000) {
135
135
  }
136
136
  async function typeCommentContent(page, content) {
137
137
  const commentInput = page.locator(COMMENT_INPUT_SELECTOR);
138
- const visibleCommentInput = await requireVisibleLocator(commentInput, '未找到评论输入框,请确认帖子详情页已正确加载。', 15_000);
138
+ const visibleCommentInput = await requireVisibleLocator(commentInput, 'Could not find the comment input. Make sure the feed detail page finished loading.', 15_000);
139
139
  await visibleCommentInput.scrollIntoViewIfNeeded();
140
140
  await visibleCommentInput.click({
141
141
  force: true
@@ -160,7 +160,7 @@ async function clickSendComment(page) {
160
160
  const sendButton = page.locator(COMMENT_SEND_BUTTON_SELECTOR).filter({
161
161
  hasText: COMMENT_SEND_BUTTON_TEXT
162
162
  });
163
- const visibleSendButton = await requireVisibleLocator(sendButton, '未找到“发送”按钮,请确认评论工具栏已正确加载。', 15_000);
163
+ const visibleSendButton = await requireVisibleLocator(sendButton, 'Could not find the Send button. Make sure the comment toolbar finished loading.', 15_000);
164
164
  await page.waitForFunction(({ selector, text })=>{
165
165
  const buttons = [
166
166
  ...document.querySelectorAll(selector)
@@ -198,10 +198,10 @@ async function commentOnCurrentFeedPage(page, content) {
198
198
  async function waitForInteractContainer(page) {
199
199
  await page.waitForLoadState('domcontentloaded');
200
200
  await page.waitForTimeout(500);
201
- await requireVisibleLocator(page.locator(INTERACT_CONTAINER_SELECTOR), '未找到互动工具栏,请确认帖子详情页已正确加载。', 15_000);
201
+ await requireVisibleLocator(page.locator(INTERACT_CONTAINER_SELECTOR), 'Could not find the interaction toolbar. Make sure the feed detail page finished loading.', 15_000);
202
202
  }
203
203
  function getActionErrorMessage(action) {
204
- return action === 'like' ? '未找到点赞按钮,请确认帖子详情页已正确加载。' : '未找到收藏按钮,请确认帖子详情页已正确加载。';
204
+ return action === 'like' ? 'Could not find the Like button. Make sure the feed detail page finished loading.' : 'Could not find the Collect button. Make sure the feed detail page finished loading.';
205
205
  }
206
206
  async function ensureActionApplied(page, action, alreadyActive) {
207
207
  if (alreadyActive) {
@@ -1,8 +1,79 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
2
4
  import { parseArgs } from 'node:util';
5
+ import { fileURLToPath } from 'node:url';
3
6
  import { printJson, runCli } from '../utils/browser-cli.js';
4
7
  import { resolveStatusTarget } from './status.js';
5
8
  import { checkRednoteLogin, createRednoteSession, disconnectRednoteSession } from './checkLogin.js';
9
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+ const REDNOTE_ROOT = path.resolve(SCRIPT_DIR, '../..');
11
+ function timestampForFilename() {
12
+ return new Date().toISOString().replaceAll(':', '').replaceAll('.', '').replace('T', '-').replace('Z', 'Z');
13
+ }
14
+ function resolveQrCodePath() {
15
+ return path.join(REDNOTE_ROOT, 'output', `login-qrcode-${timestampForFilename()}.png`);
16
+ }
17
+ function parseQrCodeDataUrl(src) {
18
+ const match = src.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/);
19
+ if (!match) {
20
+ return null;
21
+ }
22
+ return {
23
+ mimeType: match[1],
24
+ buffer: Buffer.from(match[2], 'base64')
25
+ };
26
+ }
27
+ async function refreshExpiredQrCode(page) {
28
+ const statusText = page.locator('.qrcode .status-text').first();
29
+ const refreshButton = page.locator('.qrcode .status-desc.refresh').first();
30
+ const isExpiredVisible = await statusText.isVisible().catch(()=>false);
31
+ if (!isExpiredVisible) {
32
+ return false;
33
+ }
34
+ const text = (await statusText.textContent().catch(()=>null))?.trim() ?? '';
35
+ if (!text.includes('过期')) {
36
+ return false;
37
+ }
38
+ if (await refreshButton.isVisible().catch(()=>false)) {
39
+ await refreshButton.click({
40
+ timeout: 2_000
41
+ }).catch(()=>{});
42
+ await page.waitForTimeout(800);
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+ async function saveQrCodeImage(page) {
48
+ const qrImage = page.locator('.qrcode .qrcode-img').first();
49
+ for(let attempt = 0; attempt < 3; attempt += 1){
50
+ await qrImage.waitFor({
51
+ state: 'visible',
52
+ timeout: 5_000
53
+ });
54
+ const refreshed = await refreshExpiredQrCode(page);
55
+ if (refreshed) {
56
+ continue;
57
+ }
58
+ const filePath = resolveQrCodePath();
59
+ fs.mkdirSync(path.dirname(filePath), {
60
+ recursive: true
61
+ });
62
+ const src = await qrImage.getAttribute('src');
63
+ if (src) {
64
+ const parsed = parseQrCodeDataUrl(src);
65
+ if (parsed?.mimeType === 'image/png') {
66
+ fs.writeFileSync(filePath, parsed.buffer);
67
+ return filePath;
68
+ }
69
+ }
70
+ await qrImage.screenshot({
71
+ path: filePath
72
+ });
73
+ return filePath;
74
+ }
75
+ throw new Error('No usable Xiaohongshu login QR code was detected. Make sure the login dialog is open.');
76
+ }
6
77
  function printLoginHelp() {
7
78
  process.stdout.write(`rednote login
8
79
 
@@ -36,11 +107,13 @@ export async function openRednoteLogin(target, session) {
36
107
  loginClicked: false,
37
108
  pageUrl: session.page.url(),
38
109
  waitingForPhoneLogin: false,
39
- message: '当前实例已登录,无需重复执行登录操作。'
110
+ qrCodePath: null,
111
+ message: 'The current instance is already logged in. No additional login step is required.'
40
112
  }
41
113
  };
42
114
  }
43
115
  const { page } = await getOrCreateXiaohongshuPage(session);
116
+ await page.reload();
44
117
  const loginButton = page.locator('#login-btn');
45
118
  const hasLoginButton = await loginButton.count() > 0;
46
119
  if (!hasLoginButton) {
@@ -50,21 +123,25 @@ export async function openRednoteLogin(target, session) {
50
123
  loginClicked: false,
51
124
  pageUrl: page.url(),
52
125
  waitingForPhoneLogin: false,
53
- message: '未检测到登录按钮,当前实例可能已经登录。'
126
+ qrCodePath: null,
127
+ message: 'No login button was found. The current instance may already be logged in.'
54
128
  }
55
129
  };
56
130
  }
57
131
  await loginButton.first().click({
58
- timeout: 2000
132
+ timeout: 2000,
133
+ force: true
59
134
  });
60
135
  await page.waitForTimeout(500);
136
+ const qrCodePath = await saveQrCodeImage(page);
61
137
  return {
62
138
  ok: true,
63
139
  rednote: {
64
140
  loginClicked: true,
65
141
  pageUrl: page.url(),
66
142
  waitingForPhoneLogin: true,
67
- message: '已点击登录按钮,请在浏览器中继续输入手机号并完成登录。'
143
+ qrCodePath,
144
+ message: 'The login button was clicked and the QR code image was exported. Scan the code to finish logging in.'
68
145
  }
69
146
  };
70
147
  }
@@ -98,12 +98,86 @@ export function writePostsJsonl(posts, filePath) {
98
98
  const content = posts.map((post)=>JSON.stringify(post)).join('\n');
99
99
  fs.writeFileSync(filePath, content ? `${content}\n` : '', 'utf8');
100
100
  }
101
+ export function ensureJsonSavePath(format, savePath) {
102
+ if (format !== 'json') {
103
+ return;
104
+ }
105
+ if (!savePath?.trim()) {
106
+ throw new Error('The --save PATH option is required when --format json is used.');
107
+ }
108
+ }
109
+ export function resolveJsonSavePath(explicitPath) {
110
+ const normalizedPath = explicitPath?.trim();
111
+ if (!normalizedPath) {
112
+ throw new Error('The --save PATH option is required when --format json is used.');
113
+ }
114
+ return path.resolve(normalizedPath);
115
+ }
116
+ export function writeJsonFile(payload, filePath) {
117
+ fs.mkdirSync(path.dirname(filePath), {
118
+ recursive: true
119
+ });
120
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
121
+ }
122
+ function describeStringValue(value, key) {
123
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value) || key.toLowerCase().endsWith('at')) {
124
+ return 'ISO-8601 string';
125
+ }
126
+ if (key.toLowerCase().endsWith('url')) {
127
+ return 'string (URL)';
128
+ }
129
+ if (key.toLowerCase().endsWith('path')) {
130
+ return 'string (path)';
131
+ }
132
+ return 'string';
133
+ }
134
+ function buildJsonFieldExample(value, key = '', depth = 0) {
135
+ if (value === null) {
136
+ return 'null';
137
+ }
138
+ if (value === undefined) {
139
+ return 'undefined';
140
+ }
141
+ if (typeof value === 'string') {
142
+ return describeStringValue(value, key);
143
+ }
144
+ if (typeof value === 'number') {
145
+ return 'number';
146
+ }
147
+ if (typeof value === 'boolean') {
148
+ return 'boolean';
149
+ }
150
+ if (Array.isArray(value)) {
151
+ return value.length > 0 ? [
152
+ buildJsonFieldExample(value[0], key, depth + 1)
153
+ ] : [
154
+ 'unknown'
155
+ ];
156
+ }
157
+ if (typeof value === 'object') {
158
+ if (depth >= 3 || key === 'raw') {
159
+ return 'object';
160
+ }
161
+ const entries = Object.entries(value);
162
+ if (entries.length === 0) {
163
+ return 'object';
164
+ }
165
+ return Object.fromEntries(entries.map(([entryKey, entryValue])=>[
166
+ entryKey,
167
+ buildJsonFieldExample(entryValue, entryKey, depth + 1)
168
+ ]));
169
+ }
170
+ return typeof value;
171
+ }
172
+ export function renderJsonSaveSummary(filePath, payload) {
173
+ return `Saved JSON: ${filePath}\n\nField format example:\n${JSON.stringify(buildJsonFieldExample(payload), null, 3)}\n`;
174
+ }
101
175
  function formatField(value) {
102
176
  return value ?? '';
103
177
  }
104
178
  export function renderPostsMarkdown(posts) {
105
179
  if (posts.length === 0) {
106
- return '没有获取到帖子。\n';
180
+ return 'No posts were captured.\n';
107
181
  }
108
182
  return `${posts.map((post)=>[
109
183
  `- id: ${post.id}`,