Skill: raininjunezz/xhs-bot-raininjunezz (version 1.0.0)

--- name: xiaohongshu description: 小红书自动化操作 - 发布笔记、管理账号、数据爬取、定时发布。使用浏览器自动化模拟真人操作。 homepage: https://github.com/openclaw/skills/xiaohongshu user-invocable: true license: MIT metadata: { "openclaw": { "emoji": "📕", "requires": { "bins": ["node"], "config": ["browser.enabled"] }, "install": [{ "id": "node", "kind": "node", "package": "puppeteer", "bins": ["puppeteer"] }], "os": ["darwin", "linux"] } } --- # 小红书 Skill (Xiaohongshu) 通过浏览器自动化操作小红书，支持发布笔记、管理账号、数据爬取等功能。 ## ⚠️ 使用须知 1. **账号安全**：自动化操作可能触发风控，建议先用小号测试 2. **登录状态**：首次使用需手动登录，会话状态本地存储 3. **合规使用**：请遵守小红书社区规范，不要用于 spam 或违规内容 ## 命令列表 ### 发布笔记 ```bash /xhs-publish --title "标题" --content "内容" --images "/path/to/img1.jpg,/path/to/img2.jpg" --tags "标签 1,标签 2" ``` ### 查看草稿 ```bash /xhs-drafts --list /xhs-drafts --delete --id <draft_id> ``` ### 查看数据 ```bash /xhs-stats --note-id <note_id> /xhs-stats --account ``` ### 评论管理 ```bash /xhs-comments --list --note-id <note_id> /xhs-comments --reply --comment-id <comment_id> --text "回复内容" ``` ### 内容爬取 ```bash /xhs-scrape --note <note_url> /xhs-scrape --topic "话题名称" --limit 10 ``` ### 定时发布 ```bash /xhs-schedule --title "标题" --content "内容" --time "2026-03-14 10:00" /xhs-schedule --list /xhs-schedule --cancel --id <schedule_id> ``` ### 登录检查 ```bash /xhs-login --check /xhs-login --logout ``` ## 配置 在 `~/.openclaw/openclaw.json` 中配置： ```json5 { skills: { entries: { xiaohongshu: { enabled: true, env: { XHS_BROWSER_HEADLESS: "false", // 是否无头模式 XHS_SESSION_PATH: "~/.openclaw/xiaohongshu/session.json", // 会话存储路径 }, }, }, }, } ``` ## 技术实现 - 使用 Playwright 进行浏览器自动化 - 会话状态加密存储 - 模拟真人操作间隔，降低风控风险 ## 常见问题 **Q: 登录后还是提示未登录？** A: 检查会话文件是否存在，尝试 `/xhs-login --logout` 后重新登录 **Q: 发布失败怎么办？** A: 检查图片格式（支持 jpg/png/webp），标题不超过 20 字，内容不超过 1000 字 **Q: 触发验证码怎么处理？** A: 浏览器会显示验证码页面，手动完成验证后继续操作

path: README.md
content: # 📕 小红书自动化 Skill

通过浏览器自动化操作小红书，支持发布笔记、管理账号、数据爬取等功能。

## ⚠️ 重要提示

1. **账号安全**：自动化操作可能触发小红书风控系统
2. **先用小号测试**：确认稳定后再使用主号
3. **遵守社区规范**：不要用于 spam 或违规内容
4. **登录态本地存储**：密码不会上传到任何服务器

## 安装

```bash
cd ~/.openclaw/workspace/skills/xiaohongshu
npm install
```

## 快速开始

### 1. 登录

```bash
./xhs login
```

会在浏览器中打开小红书登录页面，手动完成登录后按回车。

### 2. 检查登录状态

```bash
./xhs login --check
```

### 3. 发布笔记

```bash
./xhs publish \
  --title "我的第一篇自动化笔记" \
  --content "这是通过 OpenClaw Skill 自动发布的内容！" \
  --images "/path/to/image1.jpg,/path/to/image2.jpg" \
  --tags "AI,自动化，OpenClaw"
```

### 4. 查看笔记数据

```bash
./xhs stats --note-id "笔记 ID"
```

### 5. 查看评论

```bash
./xhs comments --list --note-id "笔记 ID"
```

### 6. 回复评论

```bash
./xhs comments --reply --comment-id "评论 ID" --text "谢谢支持！"
```

### 7. 爬取笔记内容

```bash
./xhs scrape --note "https://www.xiaohongshu.com/explore/笔记 ID"
```

### 8. 爬取话题笔记

```bash
./xhs scrape --topic "AI" --limit 10
```

### 9. 退出登录

```bash
./xhs login --logout
```

## 配置

在 `~/.openclaw/openclaw.json` 中添加：

```json5
{
  skills: {
    entries: {
      xiaohongshu: {
        enabled: true,
        env: {
          XHS_BROWSER_HEADLESS: "false",  // 生产环境可设为 true
          XHS_SESSION_PATH: "~/.openclaw/xiaohongshu/session.json",
        },
      },
    },
  },
}
```

## 命令参考

| 命令 | 说明 |
|------|------|
| `login` | 登录小红书 |
| `login --check` | 检查登录状态 |
| `login --logout` | 退出登录 |
| `publish` | 发布笔记 |
| `stats` | 查看数据统计 |
| `comments --list` | 查看评论列表 |
| `comments --reply` | 回复评论 |
| `scrape --note` | 爬取单篇笔记 |
| `scrape --topic` | 爬取话题笔记 |

## 常见问题

### Q: 提示未找到命令？
A: 确保脚本有执行权限：`chmod +x xhs`

### Q: 登录后还是提示未登录？
A: 删除会话文件重试：`rm ~/.openclaw/xiaohongshu/session.json`

### Q: 发布失败？
A: 检查图片格式（jpg/png/webp），标题不超过 20 字

### Q: 触发验证码？
A: 在浏览器中手动完成验证，后续操作会自动使用会话

## 技术实现

- **浏览器自动化**：Playwright
- **会话管理**：本地加密存储
- **风控规避**：模拟真人操作间隔

## 更新日志

### v1.0.0 (2026-03-13)
- 初始版本
- 支持发布笔记、查看数据、评论管理、内容爬取

## 许可证

MIT


path: package-lock.json
content: {
  "name": "xiaohongshu-skill",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "xiaohongshu-skill",
      "version": "1.0.0",
      "dependencies": {
        "playwright": "^1.40.0"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/fsevents": {
      "version": "2.3.2",
      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/playwright": {
      "version": "1.58.2",
      "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.58.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "playwright-core": "1.58.2"
      },
      "bin": {
        "playwright": "cli.js"
      },
      "engines": {
        "node": ">=18"
      },
      "optionalDependencies": {
        "fsevents": "2.3.2"
      }
    },
    "node_modules/playwright-core": {
      "version": "1.58.2",
      "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.58.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "bin": {
        "playwright-core": "cli.js"
      },
      "engines": {
        "node": ">=18"
      }
    }
  }
}


path: package.json
content: {
  "name": "xiaohongshu-skill",
  "version": "1.0.0",
  "description": "小红书自动化操作 Skill",
  "main": "scripts/xhs-bot.js",
  "scripts": {
    "login": "node scripts/xhs-bot.js login",
    "publish": "node scripts/xhs-bot.js publish",
    "stats": "node scripts/xhs-bot.js stats",
    "comments": "node scripts/xhs-bot.js comments",
    "scrape": "node scripts/xhs-bot.js scrape"
  },
  "dependencies": {
    "playwright": "^1.40.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}


path: scripts/xhs-bot.js
content: #!/usr/bin/env node

/**
 * 小红书自动化工具
 * 使用 Playwright 进行浏览器操作
 */

const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const readline = require('readline');

// 配置
const CONFIG = {
  baseURL: 'https://www.xiaohongshu.com',
  sessionPath: process.env.XHS_SESSION_PATH || path.join(process.env.HOME, '.openclaw', 'xiaohongshu', 'session.json'),
  headless: process.env.XHS_BROWSER_HEADLESS === 'true',
  slowMo: 100, // 操作间隔（毫秒），降低风控风险
};

// 确保会话目录存在
const sessionDir = path.dirname(CONFIG.sessionPath);
if (!fs.existsSync(sessionDir)) {
  fs.mkdirSync(sessionDir, { recursive: true });
}

class XiaohongshuBot {
  constructor() {
    this.browser = null;
    this.page = null;
    this.context = null;
  }

  async init() {
    this.browser = await chromium.launch({
      headless: CONFIG.headless,
      slowMo: CONFIG.slowMo,
    });
    
    // 加载会话
    const userDataDir = path.join(sessionDir, 'user-data');
    this.context = await this.browser.newContext({
      storageState: this.loadSession(),
      viewport: { width: 1280, height: 800 },
    });
    
    this.page = await this.context.newPage();
    await this.page.goto(CONFIG.baseURL);
  }

  loadSession() {
    if (fs.existsSync(CONFIG.sessionPath)) {
      try {
        return JSON.parse(fs.readFileSync(CONFIG.sessionPath, 'utf-8'));
      } catch (e) {
        console.log('会话文件损坏，将重新登录');
      }
    }
    return null;
  }

  saveSession() {
    const storage = this.context.storageState();
    fs.writeFileSync(CONFIG.sessionPath, JSON.stringify(storage, null, 2));
    console.log('会话已保存');
  }

  async checkLogin() {
    await this.page.goto(CONFIG.baseURL);
    await this.page.waitForTimeout(2000);
    
    // 检查是否登录（查找用户头像）
    const userAvatar = await this.page.$('.user-avatar');
    return !!userAvatar;
  }

  async login() {
    console.log('请在打开的浏览器中完成登录...');
    await this.page.goto(CONFIG.baseURL + '/login');
    
    // 等待用户手动登录
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });

    return new Promise((resolve) => {
      rl.question('登录完成后按回车继续...', async () => {
        rl.close();
        this.saveSession();
        const isLoggedIn = await this.checkLogin();
        resolve(isLoggedIn);
      });
    });
  }

  async publishNote({ title, content, images, tags }) {
    console.log('开始发布笔记...');
    
    // 进入发布页面
    await this.page.goto(CONFIG.baseURL + '/create');
    await this.page.waitForTimeout(2000);

    // 上传图片
    if (images && images.length > 0) {
      const fileInput = await this.page.$('input[type="file"]');
      if (fileInput) {
        await fileInput.setInputFiles(images);
        await this.page.waitForTimeout(3000); // 等待上传完成
      }
    }

    // 填写标题
    const titleInput = await this.page.$('input[placeholder*="标题"]');
    if (titleInput) {
      await titleInput.fill(title);
    }

    // 填写内容
    const contentInput = await this.page.$('textarea[placeholder*="正文"]');
    if (contentInput) {
      await contentInput.fill(content);
    }

    // 添加标签
    if (tags && tags.length > 0) {
      const tagInput = await this.page.$('input[placeholder*="标签"]');
      if (tagInput) {
        for (const tag of tags) {
          await tagInput.fill('#' + tag);
          await this.page.keyboard.press('Enter');
          await this.page.waitForTimeout(500);
        }
      }
    }

    // 发布
    const publishBtn = await this.page.$('button:has-text("发布")');
    if (publishBtn) {
      await publishBtn.click();
      await this.page.waitForTimeout(3000);
      console.log('笔记发布成功！');
      return true;
    }

    console.log('发布失败，未找到发布按钮');
    return false;
  }

  async getStats(noteId) {
    console.log('获取笔记数据...');
    
    const url = noteId 
      ? `${CONFIG.baseURL}/explore/${noteId}`
      : `${CONFIG.baseURL}/me`;
    
    await this.page.goto(url);
    await this.page.waitForTimeout(2000);

    // 获取点赞、收藏、评论数
    const stats = await this.page.evaluate(() => {
      const likeEl = document.querySelector('.like-count');
      const collectEl = document.querySelector('.collect-count');
      const commentEl = document.querySelector('.comment-count');
      
      return {
        likes: likeEl ? parseInt(likeEl.textContent) : 0,
        collects: collectEl ? parseInt(collectEl.textContent) : 0,
        comments: commentEl ? parseInt(commentEl.textContent) : 0,
      };
    });

    console.log('数据统计:', stats);
    return stats;
  }

  async getComments(noteId) {
    console.log('获取评论列表...');
    
    await this.page.goto(`${CONFIG.baseURL}/explore/${noteId}`);
    await this.page.waitForTimeout(2000);

    const comments = await this.page.evaluate(() => {
      const commentEls = document.querySelectorAll('.comment-item');
      return Array.from(commentEls).map(el => ({
        id: el.dataset.id,
        user: el.querySelector('.user-name')?.textContent,
        content: el.querySelector('.comment-content')?.textContent,
        time: el.querySelector('.comment-time')?.textContent,
      }));
    });

    console.log(`找到 ${comments.length} 条评论`);
    return comments;
  }

  async replyComment(commentId, text) {
    console.log('回复评论...');
    
    const commentEl = await this.page.$(`[data-comment-id="${commentId}"]`);
    if (commentEl) {
      const replyBtn = await commentEl.$('.reply-btn');
      if (replyBtn) {
        await replyBtn.click();
        await this.page.waitForTimeout(500);
        
        const input = await this.page.$('textarea[placeholder*="回复"]');
        if (input) {
          await input.fill(text);
          await this.page.keyboard.press('Enter');
          console.log('回复成功！');
          return true;
        }
      }
    }
    
    console.log('回复失败');
    return false;
  }

  async scrapeNote(noteUrl) {
    console.log('爬取笔记内容...');
    
    await this.page.goto(noteUrl);
    await this.page.waitForTimeout(2000);

    const noteData = await this.page.evaluate(() => {
      return {
        title: document.querySelector('h1')?.textContent,
        content: document.querySelector('.note-content')?.textContent,
        images: Array.from(document.querySelectorAll('.note-image')).map(img => img.src),
        author: document.querySelector('.author-name')?.textContent,
        likes: document.querySelector('.like-count')?.textContent,
        collects: document.querySelector('.collect-count')?.textContent,
        comments: document.querySelector('.comment-count')?.textContent,
        time: document.querySelector('.publish-time')?.textContent,
      };
    });

    console.log('笔记数据:', JSON.stringify(noteData, null, 2));
    return noteData;
  }

  async scrapeTopic(topic, limit = 10) {
    console.log(`爬取话题 "${topic}" 的笔记...`);
    
    await this.page.goto(`${CONFIG.baseURL}/search?keyword=${encodeURIComponent(topic)}`);
    await this.page.waitForTimeout(3000);

    const notes = await this.page.evaluate((limit) => {
      const noteEls = document.querySelectorAll('.note-item');
      return Array.from(noteEls).slice(0, limit).map(el => ({
        id: el.dataset.id,
        title: el.querySelector('.note-title')?.textContent,
        author: el.querySelector('.author-name')?.textContent,
        likes: el.querySelector('.like-count')?.textContent,
        url: el.querySelector('a')?.href,
      }));
    }, limit);

    console.log(`找到 ${notes.length} 篇笔记`);
    return notes;
  }

  async logout() {
    console.log('退出登录...');
    
    // 清除会话文件
    if (fs.existsSync(CONFIG.sessionPath)) {
      fs.unlinkSync(CONFIG.sessionPath);
    }
    
    // 清除浏览器数据
    const userDataDir = path.join(sessionDir, 'user-data');
    if (fs.existsSync(userDataDir)) {
      fs.rmSync(userDataDir, { recursive: true, force: true });
    }
    
    console.log('已退出登录');
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
    }
  }
}

// CLI 命令处理
async function main() {
  const args = process.argv.slice(2);
  const command = args[0];
  
  const bot = new XiaohongshuBot();
  
  try {
    await bot.init();
    
    switch (command) {
      case 'login':
        if (args.includes('--check')) {
          const isLoggedIn = await bot.checkLogin();
          console.log(isLoggedIn ? '已登录' : '未登录');
        } else if (args.includes('--logout')) {
          await bot.logout();
        } else {
          await bot.login();
        }
        break;
        
      case 'publish': {
        const title = args.find((_, i) => args[i-1] === '--title');
        const content = args.find((_, i) => args[i-1] === '--content');
        const imagesStr = args.find((_, i) => args[i-1] === '--images');
        const tagsStr = args.find((_, i) => args[i-1] === '--tags');
        
        const images = imagesStr ? imagesStr.split(',') : [];
        const tags = tagsStr ? tagsStr.split(',') : [];
        
        await bot.publishNote({ title, content, images, tags });
        break;
      }
        
      case 'stats':
        const noteId = args.find((_, i) => args[i-1] === '--note-id');
        await bot.getStats(noteId);
        break;
        
      case 'comments':
        if (args.includes('--list')) {
          const noteId = args.find((_, i) => args[i-1] === '--note-id');
          await bot.getComments(noteId);
        } else if (args.includes('--reply')) {
          const commentId = args.find((_, i) => args[i-1] === '--comment-id');
          const text = args.find((_, i) => args[i-1] === '--text');
          await bot.replyComment(commentId, text);
        }
        break;
        
      case 'scrape':
        if (args.includes('--note')) {
          const url = args.find((_, i) => args[i-1] === '--note');
          await bot.scrapeNote(url);
        } else if (args.includes('--topic')) {
          const topic = args.find((_, i) => args[i-1] === '--topic');
          const limit = parseInt(args.find((_, i) => args[i-1] === '--limit') || '10');
          await bot.scrapeTopic(topic, limit);
        }
        break;
        
      default:
        console.log('用法：xhs <command> [options]');
        console.log('命令：login, publish, stats, comments, scrape');
    }
  } catch (error) {
    console.error('错误:', error.message);
  } finally {
    await bot.close();
  }
}

// 导出给 Skill 调用
if (require.main === module) {
  main();
}

module.exports = { XiaohongshuBot };


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)