Skill: 279458179/xhsmander (version 1.0.1)

--- name: xhsmander description: | 小红书自动化发布技能。通过 Docker 容器中的 xiaohongshu-mcp 服务，实现登录、发布图文、搜索、互动等操作。 当用户提到发小红书、发笔记、发布内容、扫码登录小红书、小红书自动化、小红书发布时使用此技能。 --- # xhsmander - 小红书定制化发布技能（完整工作流） 通过 `xiaohongshu-mcp` (Docker) 提供完整的小红书自动化能力。 ## 文件结构 ``` xhsmander/ ├── SKILL.md # 本技能说明 ├── docker-compose.yml # Docker 启动配置 ├── _meta.json # 元数据 ├── scripts/ │ ├── mcp_dispatcher.py # MCP 调度器（核心） │ ├── check_login.py # 检查登录状态 │ ├── get_qrcode.py # 获取登录二维码 │ ├── publish.py # 发布图文（原始） │ ├── search.py # 搜索内容 │ ├── cli.py # CLI 工具 │ ├── __init__.py │ ├── check_status.py # 检查登录状态（简化版） │ ├── get_qr.py # 获取二维码并保存（简化版） │ └── publish_post.py # 发布图文（直接运行版） └── references/ └── mcp_api.md # MCP API 参考 ``` ## 快速部署 ### 1. 启动 Docker 容器 ```bash cd skills/xhsmander docker compose up -d ``` ### 2. 检查服务状态 ```bash python scripts/check_status.py ``` 输出 `Done` 即表示服务正常运行。 ### 3. 首次登录（获取二维码） ```bash python scripts/get_qr.py ``` 二维码保存在 `scripts/qrcode.png`，发送给用户扫码登录。 ### 4. 发布图文笔记 编辑 `scripts/publish_post.py` 中的 title、content、images 路径，然后运行： ```bash python scripts/publish_post.py ``` ## 核心脚本 ### mcp_dispatcher.py `MCP HTTP+JSON-RPC` 调度器，每次调用自动处理 initialize + session 管理。 ### publish_post.py（直接运行版） 封装好的发布脚本，直接修改顶部变量即可发布： ```python title = "笔记标题（≤20字）" content = "笔记正文（≤1000字）" images = ['/app/images/your_image.png'] # 容器内路径 tags = ["标签1", "标签2"] ``` **图片路径规则：** - 图片必须放在 `scripts/images/` 目录 - 容器内路径为 `/app/images/xxx.png` - 本机路径需映射到容器内路径 ### check_status.py 简化版登录状态检查，运行后结果保存在 `scripts/login_status.json`。 ### get_qr.py 简化版二维码获取，运行后： - 二维码图片：`scripts/qrcode.png` - 完整响应：`scripts/qrcode_result.json` ## 架构说明 ``` 本机(OpenClaw) --HTTP+JSON-RPC--> Docker容器(xiaohongshu-mcp) --Chrome/ROD--> 小红书网页 ``` **关键路径规则：** - 本机路径（如图片）→ 容器无法直接访问 - 容器内路径 `/app/images/` → 本机 `scripts/images/` 目录（docker-compose 挂载） - 发布图片时，images 参数传容器内路径 `/app/images/xxx.png` ## MCP API 工具列表 | 工具名 | 用途 | 关键参数 | |---|---|---| | `check_login_status` | 检查登录状态 | 无 | | `get_login_qrcode` | 获取登录二维码 | 无 | | `publish_content` | 发布图文 | title, content, images, tags | | `search_feeds` | 搜索笔记 | keyword | | `list_feeds` | 首页推荐 | 无 | | `like_feed` | 点赞 | feed_id, xsec_token | | `favorite_feed` | 收藏 | feed_id, xsec_token | | `get_feed_detail` | 笔记详情 | feed_id, xsec_token | | `user_profile` | 用户主页 | user_id, xsec_token | | `post_comment_to_feed` | 评论 | feed_id, xsec_token, content | ## 限制与注意事项 1. **Session 不可复用**：每次 MCP 请求前必须先发 initialize 获取新 session ID 2. **图片路径**：必须是 `/app/images/xxx.png`（容器内路径），不是本机路径 3. **Cookie 有效期**：Cookie 存储在 Docker volume 中，重启容器需重新登录 4. **二维码时效**：有效期约5分钟，超时需重新获取 5. **发布限制**：每天发帖量建议≤50篇 6. **标题限制**：≤20字 7. **正文限制**：≤1000字 ## Docker 环境信息 - 容器名：`xiaohongshu-mcp` - 端口：18060 - 镜像：`xpzouying/xiaohongshu-mcp` - 数据卷：`./data`（cookies）、`./images`（发布图片） - 挂载配置：见 `docker-compose.yml`

path: docker-compose.yml
content: services:
  xiaohongshu-mcp:
    # Docker Hub 镜像（默认）
    image: xpzouying/xiaohongshu-mcp
    # 阿里云镜像源（国内用户推荐，拉取更快）
    # image: crpi-hocnvtkomt7w9v8t.cn-beijing.personal.cr.aliyuncs.com/xpzouying/xiaohongshu-mcp
    container_name: xiaohongshu-mcp
    restart: unless-stopped
    init: true
    tty: true
    volumes:
      - ./data:/app/data
      - ./images:/app/images
    environment:
      - ROD_BROWSER_BIN=/usr/bin/google-chrome
      - COOKIES_PATH=/app/data/cookies.json
    ports:
      - "18060:18060"

path: _meta.json
content: ownerId: kn791bb6rb4bqnx6p60qtde4ed80nhc7
slug: xhsmander
version: 1.0.0
publishedAt: 1775891250322

path: scripts/check_login.py
content: """
检查登录状态
"""
import sys
import os
import json
sys.path.insert(0, os.path.dirname(__file__))

from mcp_dispatcher import call_tool, check_running

if __name__ == "__main__":
    if not check_running():
        print(json.dumps({"error": "MCP service not running"}))
        sys.exit(1)
    result = call_tool("check_login_status")
    for item in result:
        if item.get("type") == "text":
            print(item["text"])


path: scripts/check_status.py
content: import urllib.request
import json
import sys

url = 'http://localhost:18060/mcp'
headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json, text/event-stream'
}

# Initialize
payload = {
    'jsonrpc': '2.0',
    'method': 'initialize',
    'params': {},
    'id': 1
}
req = urllib.request.Request(
    url,
    data=json.dumps(payload).encode(),
    headers=headers,
    method='POST'
)
with urllib.request.urlopen(req, timeout=10) as resp:
    session_id = resp.headers.get('Mcp-Session-Id')

headers['Mcp-Session-Id'] = session_id

# Call check_login_status
payload = {
    'jsonrpc': '2.0',
    'method': 'tools/call',
    'params': {
        'name': 'check_login_status',
        'arguments': {}
    },
    'id': 2
}
req = urllib.request.Request(
    url,
    data=json.dumps(payload).encode(),
    headers=headers,
    method='POST'
)
with urllib.request.urlopen(req, timeout=60) as resp:
    result = json.loads(resp.read().decode())

with open(r'C:\Users\15822\.agents\skills\xhsmander\scripts\login_status.json', 'w', encoding='utf-8') as f:
    json.dump(result, f, ensure_ascii=False, indent=2)

print('Done')


path: scripts/cli.py
content: """
xhsmander CLI - 统一的 MCP 操作入口

用法:
  python cli.py check-login
  python cli.py get-qrcode
  python cli.py publish "<标题>" "<正文>" "<容器图片路径>" "<标签1,标签2>"
  python cli.py search "<关键词>" [数量]
  python cli.py list-feeds [数量]
  python cli.py like <feed_id> <xsec_token>
  python cli.py favorite <feed_id> <xsec_token>
"""
import sys
import os
import json

sys.path.insert(0, os.path.dirname(__file__))

from mcp_dispatcher import call_tool, check_running

COMMANDS = {
    "check-login":    ("check_login_status", {}),
    "get-qrcode":     ("get_login_qrcode", {}),
}

def cmd_check_login():
    result = call_tool("check_login_status")
    for item in result:
        if item.get("type") == "text":
            print(item["text"])

def cmd_get_qrcode():
    import base64
    result = call_tool("get_login_qrcode")
    for item in result:
        if item.get("type") == "text":
            print(item["text"])
        elif item.get("type") == "image":
            data = item.get("data")
            if data:
                img_bytes = base64.b64decode(data)
                out_path = os.path.join(os.path.dirname(__file__), "login_qrcode.png")
                with open(out_path, "wb") as f:
                    f.write(img_bytes)
                print(f"QR saved: {out_path}")

def cmd_publish(title, content, image_path, tags_csv):
    tags = [t.strip() for t in tags_csv.split(",") if t.strip()]
    result = call_tool("publish_content", {
        "title": title,
        "content": content,
        "images": [image_path],
        "tags": tags
    })
    for item in result:
        if item.get("type") == "text":
            print(item["text"])

def cmd_search(keyword, limit=10):
    result = call_tool("search_feeds", {"keyword": keyword, "limit": limit})
    for item in result:
        if item.get("type") == "text":
            print(item["text"])

def cmd_list_feeds(limit=10):
    result = call_tool("list_feeds", {"limit": limit})
    for item in result:
        if item.get("type") == "text":
            print(item["text"])

def cmd_like(feed_id, xsec_token):
    result = call_tool("like_feed", {"feed_id": feed_id, "xsec_token": xsec_token})
    for item in result:
        if item.get("type") == "text":
            print(item["text"])

def cmd_favorite(feed_id, xsec_token):
    result = call_tool("favorite_feed", {"feed_id": feed_id, "xsec_token": xsec_token})
    for item in result:
        if item.get("type") == "text":
            print(item["text"])

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)
    
    if not check_running():
        print("ERROR: MCP service not running. Start Docker first.")
        sys.exit(1)
    
    cmd = sys.argv[1]
    
    if cmd == "check-login":
        cmd_check_login()
    elif cmd == "get-qrcode":
        cmd_get_qrcode()
    elif cmd == "publish":
        if len(sys.argv) < 6:
            print("Usage: cli.py publish <title> <content> <image_path> <tags_csv>")
            sys.exit(1)
        cmd_publish(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
    elif cmd == "search":
        if len(sys.argv) < 3:
            print("Usage: cli.py search <keyword> [limit]")
            sys.exit(1)
        limit = int(sys.argv[3]) if len(sys.argv) > 3 else 10
        cmd_search(sys.argv[2], limit)
    elif cmd == "list-feeds":
        limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
        cmd_list_feeds(limit)
    elif cmd == "like":
        if len(sys.argv) < 4:
            print("Usage: cli.py like <feed_id> <xsec_token>")
            sys.exit(1)
        cmd_like(sys.argv[2], sys.argv[3])
    elif cmd == "favorite":
        if len(sys.argv) < 4:
            print("Usage: cli.py favorite <feed_id> <xsec_token>")
            sys.exit(1)
        cmd_favorite(sys.argv[2], sys.argv[3])
    else:
        print(f"Unknown command: {cmd}")
        print(__doc__)
        sys.exit(1)


path: scripts/get_qr.py
content: import urllib.request
import json
import base64

url = 'http://localhost:18060/mcp'
headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json, text/event-stream'
}

# Initialize
payload = {
    'jsonrpc': '2.0',
    'method': 'initialize',
    'params': {},
    'id': 1
}
req = urllib.request.Request(
    url,
    data=json.dumps(payload).encode(),
    headers=headers,
    method='POST'
)
with urllib.request.urlopen(req, timeout=10) as resp:
    session_id = resp.headers.get('Mcp-Session-Id')

headers['Mcp-Session-Id'] = session_id

# Call get_login_qrcode
payload = {
    'jsonrpc': '2.0',
    'method': 'tools/call',
    'params': {
        'name': 'get_login_qrcode',
        'arguments': {}
    },
    'id': 2
}
req = urllib.request.Request(
    url,
    data=json.dumps(payload).encode(),
    headers=headers,
    method='POST'
)
with urllib.request.urlopen(req, timeout=60) as resp:
    result = json.loads(resp.read().decode())

with open(r'C:\Users\15822\.agents\skills\xhsmander\scripts\qrcode_result.json', 'w', encoding='utf-8') as f:
    json.dump(result, f, ensure_ascii=False, indent=2)

# Extract and save QR code image
if 'result' in result and 'content' in result['result']:
    for item in result['result']['content']:
        if item.get('type') == 'image':
            image_data = item.get('data', '')
            if image_data:
                image_bytes = base64.b64decode(image_data)
                with open(r'C:\Users\15822\.agents\skills\xhsmander\scripts\qrcode.png', 'wb') as f:
                    f.write(image_bytes)
                print('QR code saved to qrcode.png')
                break

print('Done')


path: scripts/get_qrcode.py
content: """
获取登录二维码并保存为图片
输出：保存路径 + 过期时间
"""
import sys
import os
import base64
import json
sys.path.insert(0, os.path.dirname(__file__))

from mcp_dispatcher import call_tool, check_running

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
QR_OUTPUT = os.path.join(SCRIPT_DIR, "login_qrcode.png")

if __name__ == "__main__":
    if not check_running():
        print("MCP service not running")
        sys.exit(1)
    
    result = call_tool("get_login_qrcode")
    
    image_data = None
    expire_time = None
    
    for item in result:
        if item.get("type") == "text":
            # 提取过期时间
            text = item["text"]
            print(text)
        elif item.get("type") == "image":
            image_data = item.get("data")
    
    if image_data:
        img_bytes = base64.b64decode(image_data)
        with open(QR_OUTPUT, "wb") as f:
            f.write(img_bytes)
        print(f"QR code saved to: {QR_OUTPUT}")
        print(f"Image size: {len(img_bytes)} bytes")
    else:
        print("No QR code in response")
        sys.exit(1)


path: scripts/mcp_dispatcher.py
content: """
xiaohongshu-mcp HTTP+JSON-RPC 调度器
每次调用自动处理 initialize + session 管理
"""
import urllib.request
import json
import os

MCP_URL = "http://localhost:18060/mcp"

def _init():
    """初始化并返回 session_id"""
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream"
    }
    payload = {
        "jsonrpc": "2.0",
        "method": "initialize",
        "params": {
            "protocolVersion": "2024-11-05",
            "capabilities": {},
            "clientInfo": {"name": "xhsmander", "version": "1.0"}
        },
        "id": 1
    }
    req = urllib.request.Request(
        MCP_URL,
        data=json.dumps(payload).encode(),
        headers=headers,
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        json.loads(resp.read().decode())
        return resp.headers.get("Mcp-Session-Id")

def call_tool(tool_name, arguments=None):
    """
    调用 MCP 工具，返回解析后的 content 列表
    """
    if arguments is None:
        arguments = {}
    
    session_id = _init()
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
        "Mcp-Session-Id": session_id
    }
    payload = {
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": tool_name,
            "arguments": arguments
        },
        "id": 2
    }
    req = urllib.request.Request(
        MCP_URL,
        data=json.dumps(payload).encode(),
        headers=headers,
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=60) as resp:
        result = json.loads(resp.read().decode())
        if "result" in result and "content" in result["result"]:
            return result["result"]["content"]
        return result

def check_running():
    """检查 MCP 服务是否运行"""
    try:
        req = urllib.request.Request(MCP_URL, method="HEAD")
        with urllib.request.urlopen(req, timeout=3):
            return True
    except:
        return False

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("Usage: mcp_dispatcher.py <tool_name> [arg_json]")
        sys.exit(1)
    
    tool = sys.argv[1]
    args = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
    
    result = call_tool(tool, args)
    print(json.dumps(result, ensure_ascii=False))


path: scripts/publish.py
content: """
发布图文到小红书

用法: python publish.py "<标题>" "<正文>" "<图片路径(容器内)>" "<标签(逗号分隔)>"

示例:
  python publish.py "测试标题" "测试内容" "/app/images/test.png" "标签1,标签2"
"""
import sys
import os
import json
sys.path.insert(0, os.path.dirname(__file__))

from mcp_dispatcher import call_tool, check_running

if __name__ == "__main__":
    if len(sys.argv) < 5:
        print("Usage: publish.py <title> <content> <image_path> <tags_csv>")
        sys.exit(1)
    
    title = sys.argv[1]
    content = sys.argv[2]
    image_path = sys.argv[3]  # 容器内路径，如 /app/images/xxx.png
    tags = [t.strip() for t in sys.argv[4].split(",") if t.strip()]
    
    if not check_running():
        print("MCP service not running")
        sys.exit(1)
    
    result = call_tool("publish_content", {
        "title": title,
        "content": content,
        "images": [image_path],
        "tags": tags
    })
    
    for item in result:
        if item.get("type") == "text":
            print(item["text"])
        elif item.get("type") == "image":
            # 图片响应通常是上传后的预览
            print(f"[Image] mimeType: {item.get('mimeType', 'image/png')}")


path: scripts/publish_post.py
content: import urllib.request
import json

url = 'http://localhost:18060/mcp'
headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json, text/event-stream'
}

# Initialize
payload = {
    'jsonrpc': '2.0',
    'method': 'initialize',
    'params': {},
    'id': 1
}
req = urllib.request.Request(
    url,
    data=json.dumps(payload).encode(),
    headers=headers,
    method='POST'
)
with urllib.request.urlopen(req, timeout=10) as resp:
    session_id = resp.headers.get('Mcp-Session-Id')

headers['Mcp-Session-Id'] = session_id

# Publish
title = "宅家氛围感 | 午后窗边的慵懒少年"
content = """最近沉迷这种居家感✨

阳光透过窗户洒进来
配上柔软的毛绒睡袍
一副黑框眼镜
不需要刻意摆pose
松弛感拉满

这样的午后
是属于一个人的安静时光
☕️🪟

#氛围感 #少年感 #居家拍照 #男生穿搭 #斯文败类
#拍照姿势不求人 #宅家摄影 #小红书男神
#氛围感帅哥 #松弛感 #盐系男生"""

payload = {
    'jsonrpc': '2.0',
    'method': 'tools/call',
    'params': {
        'name': 'publish_content',
        'arguments': {
            'title': title,
            'content': content,
            'images': ['/app/images/post1.png'],
            'tags': ['氛围感', '少年感', '居家拍照', '男生穿搭', '斯文败类', '拍照姿势不求人', '宅家摄影', '小红书男神', '氛围感帅哥', '松弛感', '盐系男生']
        }
    },
    'id': 2
}
req = urllib.request.Request(
    url,
    data=json.dumps(payload).encode(),
    headers=headers,
    method='POST'
)
with urllib.request.urlopen(req, timeout=120) as resp:
    result = json.loads(resp.read().decode())

with open(r'C:\Users\15822\.agents\skills\xhsmander\scripts\publish_result.json', 'w', encoding='utf-8') as f:
    json.dump(result, f, ensure_ascii=False, indent=2)

print('Done')


path: scripts/search.py
content: """
搜索小红书内容

用法: python search.py "<关键词>" [数量]
"""
import sys
import os
import json
sys.path.insert(0, os.path.dirname(__file__))

from mcp_dispatcher import call_tool, check_running

if __name__ == "__main__":
    keyword = sys.argv[1] if len(sys.argv) > 1 else ""
    limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
    
    if not keyword:
        print("Usage: search.py <keyword> [limit]")
        sys.exit(1)
    
    if not check_running():
        print("MCP service not running")
        sys.exit(1)
    
    result = call_tool("search_feeds", {
        "keyword": keyword,
        "limit": limit
    })
    
    for item in result:
        if item.get("type") == "text":
            print(item["text"])


path: scripts/__init__.py
content: # xhsmander skill scripts


path: references/mcp_api.md
content: # xiaohongshu-mcp API 参考

## 基础信息

- **URL**: `http://localhost:18060/mcp`
- **协议**: JSON-RPC 2.0 over HTTP POST
- **认证**: 通过 `Mcp-Session-Id` header（每次 initialize 从响应 header 获取）

## 请求格式

```json
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "<tool_name>",
    "arguments": { ... }
  },
  "id": <number>
}
```

## 初始化（每次请求前必须）

```python
payload = {
    "jsonrpc": "2.0",
    "method": "initialize",
    "params": {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {"name": "xhsmander", "version": "1.0"}
    },
    "id": 1
}
# Session ID 从响应 header "Mcp-Session-Id" 获取
```

## 工具列表

### check_login_status
检查当前登录状态。
- arguments: `{}`
- 返回: 文本 + 是否已登录

### get_login_qrcode
获取登录二维码。
- arguments: `{}`
- 返回: 文本（过期时间）+ image（base64 PNG）

### publish_content
发布图文笔记。
- arguments:
  - `title`: string (≤20字)
  - `content`: string (≤1000字)
  - `images`: string[] (容器内路径，如 `/app/images/xxx.png`)
  - `tags`: string[]
- 注意: 图片路径必须是容器内路径，不是本机路径

### search_feeds
搜索笔记。
- arguments:
  - `keyword`: string
  - `limit`: number (默认10)

### list_feeds
获取首页推荐。
- arguments: `{}` 或 `{limit: number}`

### like_feed
点赞笔记。
- arguments:
  - `feed_id`: string（从搜索/列表结果获取）
  - `xsec_token`: string（从搜索/列表结果获取）

### favorite_feed
收藏笔记。
- arguments: 同 like_feed

### get_feed_detail
获取笔记详情（含评论）。
- arguments:
  - `feed_id`: string
  - `xsec_token`: string

### user_profile
获取用户主页。
- arguments:
  - `user_id`: string
  - `xsec_token`: string

### post_comment_to_feed
发表评论。
- arguments:
  - `feed_id`: string
  - `xsec_token`: string
  - `content`: string

## 图片路径规则

Docker 部署时，图片路径需要特殊处理：

```
本机:  scripts/images/test.png
容器:  /app/images/test.png  (docker-compose 挂载)
MCP:   images 参数传 /app/images/test.png (容器内路径)
```

复制图片到挂载目录：
```bash
cp /本地路径/image.png  scripts/images/
# 调用时传 /app/images/image.png
```

## 错误处理

- `method "tools/call" is invalid during session initialization`: 需要先调用 initialize
- `MCP service not running`: 服务未启动
- 图片文件不存在: 检查容器内路径是否正确


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