# Directory Structure
```
├── .gitignore
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│ ├── index.ts
│ └── xiaohongshu.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependency directories
node_modules/
dist/
build/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Debug files
debug-*.png
# OS specific files
.DS_Store
Thumbs.db
cookies/
results/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# RedNote MCP - Xiaohongshu Content Search Tool
## Overview
RedNote MCP is a Model Context Protocol (MCP) server for searching and retrieving content from Xiaohongshu (Red Book) platform. It provides intelligent content extraction with automatic login management and parallel processing capabilities.
## Features
- **Smart Search**: Keyword-based content search on Xiaohongshu
- **Auto Login**: Automatic cookie management and login handling
- **Parallel Processing**: Efficient concurrent content retrieval
- **Rich Data Extraction**:
- Note titles and content
- Author information and descriptions
- Interaction metrics (likes, favorites, comments)
- Images and hashtags
- Direct note links
## Technical Stack
- **Runtime**: Node.js with TypeScript
- **Browser Automation**: Playwright
- **Protocol**: Model Context Protocol (MCP) SDK
- **Validation**: Zod schema validation
- **Package Manager**: pnpm
## Data Structure
```typescript
interface RedBookNote {
title: string; // Note title
content: string; // Note content
author: string; // Author name
authorDesc?: string; // Author description
link: string; // Note URL
likes?: number; // Like count
collects?: number; // Favorite count
comments?: number; // Comment count
tags?: string[]; // Hashtag list
images?: string[]; // Image URLs (WebP format)
}
```
## Installation
### Prerequisites
- Node.js 18+
- pnpm package manager
### Setup
1. Clone the repository:
```bash
git clone <repository-url>
cd rednote-mcp
```
2. Install dependencies:
```bash
pnpm install
```
3. Install Playwright browsers:
```bash
pnpm exec playwright install
```
4. Build the project:
```bash
pnpm build
```
## Usage
### Running the MCP Server
```bash
pnpm start
```
### Development Mode
```bash
pnpm dev
```
### Testing
```bash
pnpm test
```
## MCP Client Configuration
### Claude Desktop
Add the following configuration to your Claude Desktop config file:
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"rednote-mcp": {
"command": "node",
"args": [
"C:\\ABSOLUTE\\PATH\\TO\\rednote-mcp\\build\\index.js"
]
}
}
}
```
**For macOS/Linux users:**
```json
{
"mcpServers": {
"rednote-mcp": {
"command": "node",
"args": [
"/absolute/path/to/rednote-mcp/build/index.js"
]
}
}
}
```
Replace the path with your actual project directory.
### Other MCP Clients
For other MCP-compatible clients, use the built server file:
```bash
node build/index.js
```
## Tool Usage
Once configured, you can use the search tool in your MCP client:
```
Search for "food recommendation" on Xiaohongshu
```
The tool will return structured data including titles, content, author information, and images.
## Important Notes
- **First Run**: Manual login to Xiaohongshu is required on first use
- **Performance**: Initial searches may take 30-60 seconds due to browser startup and content loading
- **Rate Limiting**: Concurrent requests are limited to 3 to avoid platform restrictions
- **Image Format**: Images are provided in WebP format
- **Cookie Management**: Login state is automatically saved and reused
## Development
### Project Structure
```
rednote-mcp/
├── src/
│ ├── index.ts # MCP server entry point
│ └── xiaohongshu.ts # Core scraping logic
├── cookies/ # Auto-generated cookie storage
├── results/ # Optional: saved search results
├── build/ # Compiled JavaScript output
├── package.json
├── tsconfig.json
└── README.md
```
### Available Scripts
- `pnpm build` - Build TypeScript to JavaScript
- `pnpm start` - Run the built MCP server
- `pnpm dev` - Development mode with auto-reload
- `pnpm test` - Run tests (if available)
- `pnpm clean` - Clean build directory
## Troubleshooting
### Common Issues
1. **Login Required**: If you see login prompts, delete the `cookies/` directory and restart
2. **Timeout Errors**: Increase the MCP client timeout settings
3. **Browser Not Found**: Run `pnpm exec playwright install` to install browsers
4. **Permission Errors**: Ensure the project directory has proper read/write permissions
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Disclaimer
This tool is for educational and research purposes. Please respect Xiaohongshu's terms of service and rate limits when using this tool.
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "rednote-mcp",
"version": "1.0.0",
"description": "MCP server for searching and retrieving content from Xiaohongshu (Red Note) platform.",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc -w"
},
"keywords": ["mcp", "xiaohongshu", "redbook"],
"author": "",
"license": "ISC",
"packageManager": "[email protected]",
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.10.1",
"@types/node": "^22.14.1",
"playwright": "^1.52.0",
"typescript": "^5.8.3",
"zod": "^3.24.3"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { searchXiaohongshu } from "./xiaohongshu.js";
import { z } from "zod";
import { text } from "stream/consumers";
// Create MCP server
const server = new McpServer({
name: "rednote-mcp",
version: "1.1.0",
description: "MCP server for searching and retrieving content from Xiaohongshu (Red Note) platform.",
});
type ContentBlock =
| {
type: "text";
text: string;
}
| {
type: "resource";
resource: {
uri: string;
text: string;
mimeType?: string; // mimeType is optional
};
};
server.tool(
"search_xiaohongshu",
"Searches for content on Xiaohongshu (Red Note) based on a query",
{
query: z.string().describe("Search query for Xiaohongshu content"),
count: z.number().optional().default(10).describe("Number of results to return")
},
async (params: { query: string; count: number }, extra) => {
const { query, count } = params;
try {
console.error(`Searching Xiaohongshu: ${query}, Count: ${count}`);
// 1. Fetch search results from Xiaohongshu
const results = await searchXiaohongshu(query, count);
// 2. Initialize an array for content blocks with our explicit type.
const contentBlocks: ContentBlock[] = [];
// Add a main header for the search results.
contentBlocks.push({
type: "text",
text: `# Xiaohongshu Search Results for "${query}"\n\nFound ${results.length} related notes.`
});
// 3. Loop through each note to generate its corresponding text and image blocks.
for (let i = 0; i < results.length; i++) {
const note = results[i];
// --- Generate text content for the current note ---
// Requirement: Add a number to each note title.
let noteTextContent = `## ${i + 1}. ${note.title}\n\n`;
// Author information
noteTextContent += `**Author:** ${note.author}`;
if (note.authorDesc) {
noteTextContent += ` (${note.authorDesc})`;
}
noteTextContent += '\n\n';
// Interaction data
const interactionInfo = [];
if (typeof note.likes !== 'undefined') interactionInfo.push(`👍 ${note.likes}`);
if (typeof note.collects !== 'undefined') interactionInfo.push(`⭐ ${note.collects}`);
if (typeof note.comments !== 'undefined') interactionInfo.push(`💬 ${note.comments}`);
if (interactionInfo.length > 0) {
noteTextContent += `**Interactions:** ${interactionInfo.join(' · ')}\n\n`;
}
// Note content body
noteTextContent += `### Content\n${note.content.trim()}\n\n`;
// Tags
if (note.tags && note.tags.length > 0) {
noteTextContent += `**Tags:** ${note.tags.map(tag => `#${tag}`).join(' ')}\n\n`;
}
// Original Link
noteTextContent += `**Original Link:** ${note.link}`;
// Add the formatted text block to the array
contentBlocks.push({
type: "text",
text: noteTextContent
});
// --- Generate resource links for images in the current note ---
if (note.images && note.images.length > 0) {
for (let j = 0; j < note.images.length; j++) {
const imageUrl = note.images[j];
// Requirement: Number each image in its description text.
// Add each image as a separate resource link object.
contentBlocks.push({
type: "resource",
resource: {
uri: imageUrl,
// The 'text' property is required by the type definition.
text: `Image ${j + 1} for note: "${note.title}"`
}
});
}
}
// Add a separator block to visually distinguish notes.
contentBlocks.push({
type: "text",
text: "\n\n---\n\n"
});
}
// 4. Return the structured JSON object containing all content blocks.
return {
content: contentBlocks
};
} catch (error) {
console.error("Xiaohongshu search error:", error);
return {
content: [{
type: "text",
text: `Error searching Xiaohongshu content: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Start server
async function main() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Xiaohongshu MCP server started, listening for messages via stdio");
} catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
}
main();
```
--------------------------------------------------------------------------------
/src/xiaohongshu.ts:
--------------------------------------------------------------------------------
```typescript
import { chromium, Browser, BrowserContext, Page } from 'playwright';
import * as fs from 'fs/promises';
import * as path from 'path';
// RedNote interface to support new fields
export interface RedBookNote {
title: string;
content: string;
author: string;
authorDesc?: string;
link: string;
likes?: number;
collects?: number;
comments?: number;
tags?: string[];
images?: string[];
}
// Cookies file path
const COOKIES_PATH = path.join(process.cwd(), 'cookies', 'xiaohongshu-cookies.json');
// Check if cookies exist
async function cookiesExist(): Promise<boolean> {
try {
await fs.access(COOKIES_PATH);
return true;
} catch {
return false;
}
}
// Save cookies
async function saveCookies(context: BrowserContext): Promise<void> {
try {
const cookies = await context.cookies();
const cookiesDir = path.join(process.cwd(), 'cookies');
await fs.mkdir(cookiesDir, { recursive: true });
await fs.writeFile(COOKIES_PATH, JSON.stringify(cookies, null, 2));
console.error('Successfully saved cookies for next use');
} catch (error) {
console.error('Failed to save cookies:', error);
}
}
// Load cookies
async function loadCookies(context: BrowserContext): Promise<boolean> {
try {
const cookiesJson = await fs.readFile(COOKIES_PATH, 'utf-8');
const cookies = JSON.parse(cookiesJson);
await context.addCookies(cookies);
console.error('Successfully loaded cookies');
return true;
} catch (error) {
console.error('Failed to load cookies:', error);
return false;
}
}
// Check login status
async function checkLoginStatus(page: Page): Promise<boolean> {
const loginButtonSelector = '.login-container';
try {
const currentUrl = page.url();
if (currentUrl.includes('login') || currentUrl.includes('sign')) {
return false;
}
const loginButton = await page.$(loginButtonSelector);
return !loginButton;
} catch (error) {
console.error('Error checking login status:', error);
return false;
}
}
// Auto-scroll page to load more content
async function autoScroll(page: Page): Promise<void> {
await page.evaluate(async () => {
await new Promise<void>((resolve) => {
let totalHeight = 0;
const distance = 300;
const maxScrolls = 10; // Limit maximum scroll count
let scrollCount = 0;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
scrollCount++;
if (totalHeight >= scrollHeight || scrollCount >= maxScrolls) {
clearInterval(timer);
resolve();
}
}, 200); // More reasonable scroll interval
});
});
// Wait for content to load
await page.waitForTimeout(2000);
}
// Get detailed content from note detail page
async function getNoteDetailData(page: Page): Promise<{
title: string;
content: string;
author: string;
authorDesc?: string;
images: string[];
likes: number;
collects: number;
comments: number;
tags: string[];
}> {
try {
// Wait for core page elements to load
await page.waitForSelector('#detail-title', { timeout: 3000 })
.catch(() => console.error('Title element not found, page structure may have changed'));
// Extract detailed note data
return await page.evaluate(() => {
// Get title
const titleElement = document.querySelector('#detail-title');
const title = titleElement ? titleElement.textContent?.trim() || '' : '';
// Get content text
const contentElement = document.querySelector('#detail-desc .note-text');
let content = '';
if (contentElement) {
// Remove internal tag elements, keep plain text
// Copy node content instead of using innerHTML to avoid HTML tags
Array.from(contentElement.childNodes).forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
content += node.textContent;
} else if (node.nodeType === Node.ELEMENT_NODE) {
// If it's a tag node and not an a tag (tag link), add content
if ((node as Element).tagName !== 'A') {
content += node.textContent;
}
}
});
}
content = content.trim();
// Get author information
const authorElement = document.querySelector('.author-wrapper .username');
const author = authorElement ? authorElement.textContent?.trim() || '' : '';
// Try to get author description (if exists)
const authorDescElement = document.querySelector('.user-desc');
const authorDesc = authorDescElement ? authorDescElement.textContent?.trim() : undefined;
// Get image list
const imageElements = document.querySelectorAll('.note-slider-img');
const images = Array.from(imageElements).map(img => (img as HTMLImageElement).src);
// Get interaction data: likes, favorites, comments count
const interactionButtons = document.querySelectorAll('.engage-bar-style .count');
let likes = 0, collects = 0, comments = 0;
if (interactionButtons.length >= 3) {
const likesText = interactionButtons[0].textContent?.trim() || '0';
const collectsText = interactionButtons[1].textContent?.trim() || '0';
const commentsText = interactionButtons[2].textContent?.trim() || '0';
// Handle special text like "赞", convert to numbers
likes = likesText === '赞' ? 0 : parseInt(likesText) || 0;
collects = collectsText === '收藏' ? 0 : parseInt(collectsText) || 0;
comments = commentsText === '评论' ? 0 : parseInt(commentsText) || 0;
}
// Get tag list
const tagElements = document.querySelectorAll('#detail-desc .tag');
const tags = Array.from(tagElements).map(tag => tag.textContent?.trim() || '');
return {
title,
content,
author,
authorDesc,
images,
likes,
collects,
comments,
tags
};
});
} catch (error) {
console.error('Failed to extract note detail data:', error);
return {
title: '',
content: '',
author: '',
images: [],
likes: 0,
collects: 0,
comments: 0,
tags: []
};
}
}
// Get note comment data (optional)
async function getComments(page: Page, maxComments: number = 5): Promise<Array<{
author: string;
content: string;
likes: number;
images?: string[];
}>> {
try {
return await page.evaluate((max) => {
const commentItems = document.querySelectorAll('.parent-comment .comment-item');
const comments = [];
for (let i = 0; i < Math.min(commentItems.length, max); i++) {
const item = commentItems[i];
const authorElement = item.querySelector('.author .name');
const contentElement = item.querySelector('.content .note-text');
const likesElement = item.querySelector('.like .count');
// Get images in comments (if any)
const imageElements = item.querySelectorAll('.comment-picture img');
const images = Array.from(imageElements).map(img => (img as HTMLImageElement).src);
if (authorElement && contentElement) {
const author = authorElement.textContent?.trim() || '';
const content = contentElement.textContent?.trim() || '';
const likesText = likesElement?.textContent?.trim() || '0';
const likes = likesText === '赞' ? 0 : parseInt(likesText) || 0;
comments.push({
author,
content,
likes,
...(images.length > 0 ? { images } : {})
});
}
}
return comments;
}, maxComments);
} catch (error) {
console.error('Failed to extract comment data:', error);
return [];
}
}
// Extract note links from page
async function extractNoteLinks(page: Page, count: number): Promise<Array<{title: string, link: string, author: string}>> {
try {
const links = await page.evaluate((maxCount) => {
const noteElements = Array.from(document.querySelectorAll('.note-item'));
return noteElements.slice(0, maxCount).map(element => {
try {
// Extract title
const titleElement = element.querySelector('.title span') as HTMLElement;
// Extract link - try to get visible link first, then hidden link
const visibleLinkElement = element.querySelector('a.cover.mask') as HTMLAnchorElement;
const hiddenLinkElement = element.querySelector('a[style="display: none;"]') as HTMLAnchorElement;
// Extract author
const authorElement = element.querySelector('.card-bottom-wrapper .name span.name') as HTMLElement;
return {
title: titleElement ? titleElement.innerText.trim() : 'No Title',
// Link path processing: ensure link is complete URL
link: (visibleLinkElement?.href || hiddenLinkElement?.href || '')
.replace(/^\//, 'https://www.xiaohongshu.com/'),
author: authorElement ? authorElement.innerText.trim() : 'Unknown Author'
};
} catch (error) {
console.error('Error extracting note link', error);
return null;
}
});
}, count);
// Explicitly filter out null values and satisfy TypeScript type checking
return links.filter((item): item is {title: string, link: string, author: string} =>
item !== null && typeof item === 'object' && 'link' in item && !!item.link);
} catch (error) {
console.error('Failed to extract note links:', error);
return [];
}
}
// Get individual note details based on user-defined count
async function getNoteDetail(context: BrowserContext, noteInfo: {title: string, link: string, author: string}, index: number): Promise<RedBookNote> {
let notePage: Page | null = null;
try {
console.error(`Starting to get note ${index + 1} details: ${noteInfo.title}`);
notePage = await context.newPage();
// Set longer timeout
await notePage.goto(noteInfo.link, {
timeout: 30000,
waitUntil: 'domcontentloaded'
});
// Wait for page to load completely
await notePage.waitForSelector('#noteContainer', { timeout: 15000 })
.catch(() => console.error('Note container not found, trying to continue getting content'));
// Get detailed data
const detailData = await getNoteDetailData(notePage);
// // Can save screenshot for debugging
// await notePage.screenshot({ path: `note-${index + 1}.png` });
// Build complete note object
return {
title: detailData.title || noteInfo.title,
content: detailData.content || 'No content',
author: detailData.author || noteInfo.author,
authorDesc: detailData.authorDesc,
link: noteInfo.link,
likes: detailData.likes,
collects: detailData.collects,
comments: detailData.comments,
// Add enhanced data
tags: detailData.tags,
images: detailData.images
};
} catch (error) {
console.error(`Failed to get note ${index + 1} details:`, error);
// Return basic information when error occurs
return {
title: noteInfo.title,
content: 'Failed to get content',
author: noteInfo.author,
link: noteInfo.link
};
} finally {
// Ensure tab is closed
if (notePage) {
await notePage.close().catch(err => console.error('Error closing tab:', err));
}
}
}
// Extract note content from search page
async function extractNotes(page: Page, count: number, context: BrowserContext): Promise<RedBookNote[]> {
try {
// Get note links list
const noteLinks = await extractNoteLinks(page, count);
console.error(`Found ${noteLinks.length} notes, starting parallel content retrieval`);
if (noteLinks.length === 0) {
console.error('No note links found, returning empty result');
return [];
}
// Control concurrency to avoid too many concurrent requests causing blocks
const concurrency = Math.min(3, noteLinks.length);
console.error(`Setting concurrency to ${concurrency}`);
// Create task queue
const queue = [...noteLinks];
const results: RedBookNote[] = [];
// Start concurrency number of tasks simultaneously
const workers = Array(concurrency).fill(null).map(async (_, workerIndex) => {
while (queue.length > 0) {
const noteInfo = queue.shift();
if (!noteInfo) break;
const index = noteLinks.indexOf(noteInfo);
console.error(`Worker ${workerIndex+1} processing note ${index+1}`);
try {
// Slightly stagger time between workers to reduce simultaneous requests
await new Promise(resolve => setTimeout(resolve, workerIndex * 1000));
const note = await getNoteDetail(context, noteInfo, index);
if (note) {
results[index] = note; // Maintain original order
}
// Interval between requests to avoid too frequent requests
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
console.error(`Failed to process note ${index+1}:`, error);
}
}
});
// Wait for all workers to complete
await Promise.all(workers);
// Filter out undefined results and return
return results.filter(note => note !== undefined);
} catch (error) {
console.error('Failed to extract note content:', error);
return [];
}
}
// Perform search
async function performSearch(page: Page, keyword: string, count: number, context: BrowserContext): Promise<void> {
// Set longer timeout and better waiting strategy
await page.goto(`https://www.xiaohongshu.com/search_result?keyword=${encodeURIComponent(keyword)}`, {
timeout: 30000,
waitUntil: 'domcontentloaded'
});
// Wait for page to load
await page.waitForSelector('.feeds-container', { timeout: 15000 }).catch(() => {
console.error('Note list container not found, trying to wait longer');
return page.waitForTimeout(5000);
});
// If need to get more content, scroll page
if (count > 6) {
await autoScroll(page);
}
}
// Search Xiaohongshu content function
export async function searchXiaohongshu(query: string, count: number = 5): Promise<RedBookNote[]> {
let browser: Browser | null = null;
try {
const searchKeyword = query;
console.error(`Search keyword: ${searchKeyword}`);
// Check if login is needed
const needLogin = !await cookiesExist();
// Create browser instance
browser = await chromium.launch({
headless: !needLogin, // If login needed, show browser
});
if (needLogin) {
// Handle login process
const loginSuccess = await handleLogin(browser);
if (!loginSuccess) {
throw new Error('User login failed, unable to continue search');
}
}
await browser.close();
browser = await chromium.launch({ headless: true });
// Create new context for search
let context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
});
// Load cookies
await loadCookies(context);
// Create page
let page = await context.newPage();
// Verify login status
await page.goto('https://www.xiaohongshu.com/explore');
await page.waitForTimeout(5000);
const isLoggedIn = await checkLoginStatus(page);
if (!isLoggedIn) {
console.error('Cookies expired or invalid, need to login again');
// Close current browser context
await context.close();
// Need to change browser headless mode, close browser first
await browser.close();
// Restart browser in headed mode
browser = await chromium.launch({ headless: false });
// Login again
const loginSuccess = await handleLogin(browser);
if (!loginSuccess) {
throw new Error('User login failed, unable to continue search');
}
await context.close();
await browser.close();
browser = await chromium.launch({ headless: true });
// After successful login, create new search context
context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
});
// Load latest cookies
await loadCookies(context);
// Create new page
page = await context.newPage();
}
// Already logged in, proceed with search
await performSearch(page, searchKeyword, count, context);
// Get search results - pass context parameter
const notes = await extractNotes(page, count, context);
// Save latest cookies
await saveCookies(context);
return notes;
} catch (error) {
console.error('Error searching Xiaohongshu:', error);
throw error;
} finally {
// Ensure browser is closed
if (browser) {
await browser.close();
}
}
}
// Handle login process
async function handleLogin(browser: Browser): Promise<boolean> {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
viewport: { width: 1280, height: 800 },
});
let page;
try {
page = await context.newPage();
// Visit Xiaohongshu homepage
await page.goto('https://www.xiaohongshu.com/explore');
await page.waitForTimeout(5000);
console.error('Please login to Xiaohongshu in the browser, system will automatically detect login status');
// Wait for user login operation to complete
let isLoggedIn = false;
// Check login status every 2 seconds, wait up to 5 minutes
for (let i = 0; i < 150; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
isLoggedIn = await checkLoginStatus(page);
if (isLoggedIn) {
console.error('Successfully logged in detected');
break;
}
}
if (isLoggedIn) {
// Save cookies
await saveCookies(context);
return true;
} else {
console.error('Login timeout, please try again');
return false;
}
} catch (error) {
console.error('Error during user login process:', error);
return false;
} finally {
if (page) await page.close();
await context.close();
}
}
// Add function to save note data to JSON file
async function saveNotesToFile(notes: RedBookNote[], keyword: string): Promise<void> {
try {
// Create results directory
const resultsDir = path.join(process.cwd(), 'results');
await fs.mkdir(resultsDir, { recursive: true });
// Generate filename (use timestamp to avoid overwriting)
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `${resultsDir}/xiaohongshu_${encodeURIComponent(keyword)}_${timestamp}.json`;
// Write data to file
await fs.writeFile(fileName, JSON.stringify(notes, null, 2), 'utf-8');
console.error(`Note data saved to file: ${fileName}`);
} catch (error) {
console.error('Error saving note data to file:', error);
}
}
```