#
tokens: 7289/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# 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);
  }
}
```