feat(project): 初始化小红书AI爆文工坊V2.0项目

- 新增项目配置文件(.gitignore, config.json)和核心文档(Todo.md, mcp.md)
- 实现配置管理模块(config_manager.py),支持单例模式和自动保存
- 实现LLM服务模块(llm_service.py),包含文案生成、热点分析、评论回复等Prompt模板
- 实现SD服务模块(sd_service.py),封装Stable Diffusion WebUI API调用
- 实现MCP客户端模块(mcp_client.py),封装小红书MCP服务HTTP调用
- 实现主程序(main.py),构建Gradio界面,包含内容创作、热点探测、评论管家、账号登录、数据看板五大功能模块
- 保留V1版本备份(main_v1_backup.py)供参考
- 添加项目依赖文件(requirements.txt)
This commit is contained in:
zhoujie 2026-02-08 14:21:50 +08:00
commit 88faca150d
11 changed files with 3040 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
xhs_workspace
__pycache__
*.log

1
Todo.md Normal file
View File

@ -0,0 +1 @@
目前的脚本已经实现了从 “灵感 -> 文案 -> 绘图 -> 发布” 的核心闭环作为一个个人辅助工具MVP最小可行性产品已经非常出色了。但是如果要作为一个专业的运营工具或者满足商业化需求目前的版本还存在明显的短板。我将从 内容质量、运营闭环、账号安全、功能深度 四个维度为你进行全面分析,并给出升级建议。📊 当前功能评分表维度当前得分评价核心流程⭐⭐⭐⭐⭐流程跑通无需在多个软件间切换效率极大提升。内容质量⭐⭐⭐LLM 文案通用性强但个性不足SD 绘图仅支持基础生图,缺乏精细控制。运营功能⭐⭐仅支持“发”,缺乏“看”(数据分析)和“回”(评论互动)。多媒体能力⭐⭐仅支持图片,不支持视频(尽管 MCP 支持)。稳定性⭐⭐⭐依赖本地环境和 Cookie 有效期,缺乏异常重试和账号管理。🔍 深度差距分析与改进建议1. 视觉能力的局限性 (痛点:图片不可控)目前使用的是基础的 txt2img文生图。问题很难控制人物姿势、保持角色一致性比如同一个博主IP、或者在特定背景中植入产品。缺口ControlNet 支持无法指定姿势Openpose或线稿上色Canny。LoRA 切换:无法快速切换画风(如:二次元 vs 真实感 vs 胶片风。Img2Img无法基于参考图进行修改。💡 改进建议:在 UI 中增加 ControlNet 参数接口,或者增加“风格预设”下拉框(后台自动切换 LoRA。2. 缺乏“选题与热点”辅助 (痛点:不知道写什么)目前主要依赖用户自己输入“主题”。问题如果用户不知道最近什么火写的文章可能没人看。缺口MCP 搜索能力未利用xiaohongshu-mcp 有 search_feeds 功能,但脚本里没用。💡 改进建议:增加 “热点探测” tab。逻辑用户输入关键词 -> 调用 MCP 搜索 -> LLM 分析热门笔记的标题和结构 -> 生成“爆款模仿”方案。3. 缺失视频支持 (痛点:视频流量更大)小红书目前对视频流量扶持很大。问题:目前的 UI 和逻辑只支持图片。虽然 MCP 支持 publish_with_video但你没接。💡 改进建议UI 增加“上传视频”或“AI 生成视频”入口。接入 Runway / Luma API 或本地 AnimateDiff 生成几秒的动态视频。4. 运营互动的缺失 (痛点:发完不管)运营小红书,“养号”和“回复” 与发帖一样重要。问题:目前是“射后不理”。缺口:评论管理:无法自动回复评论,无法引导私域。数据反馈:发出去的笔记有多少阅读?脚本里看不到。💡 改进建议:增加 “评论管家” 模块:定期调用 MCP 获取新评论 -> LLM 生成回复 -> 调用 MCP 回复。增加 “数据看板”:调用 user_profile 展示昨日点赞涨粉数。5. 账号矩阵与安全性 (痛点:单点风险)问题:目前是单账号模式。缺口:多账号切换:如果我有 5 个号,需要反复手动扫码或替换 Cookie 文件。定时发布只能“立即发布”。真正运营需要设定在晚高峰18:00-21:00自动发。💡 改进建议:引入简单的 SQLite 数据库或 JSON 文件管理多组 Cookie。引入 APScheduler 库,实现“存入草稿箱,特定时间自动调用 MCP 发布”。🛠️ 下一步升级路线图 (Roadmap)如果你想把这个脚本升级为V2.0 专业版,建议按以下顺序添加功能:第一阶段:补全 MCP 能力 (低成本,高回报)接入搜索功能:在写文案前,先让 AI 看 5 篇同类热门笔记。接入数据面板:在侧边栏显示当前账号粉丝数、获赞数。第二阶段:增强视觉 (提升内容力)SD 进阶:支持上传参考图 (img2img)。本地图库:有时候不想用 AI 图,想混排自己拍的照片,增加“本地上传”按钮。第三阶段:自动化运营 (解放双手)自动回复机器人:根据设定的人设(知性姐姐/毒舌博主)自动回评论。定时任务:设置一个队列,让它自己跑。

10
config.json Normal file
View File

@ -0,0 +1,10 @@
{
"api_key": "sk-d212b926f51f4f0f9297629cd2ab77b4",
"base_url": "https://api.deepseek.com/v1",
"sd_url": "http://127.0.0.1:7860",
"mcp_url": "http://localhost:18060/mcp",
"model": "deepseek-reasoner",
"persona": "温柔知性的时尚博主",
"auto_reply_enabled": false,
"schedule_enabled": false
}

82
config_manager.py Normal file
View File

@ -0,0 +1,82 @@
"""
配置管理模块
支持多配置项默认值回退自动保存
"""
import json
import os
import logging
logger = logging.getLogger(__name__)
CONFIG_FILE = "config.json"
OUTPUT_DIR = "xhs_workspace"
DEFAULT_CONFIG = {
"api_key": "",
"base_url": "https://api.openai.com/v1",
"sd_url": "http://127.0.0.1:7860",
"mcp_url": "http://localhost:18060/mcp",
"model": "gpt-3.5-turbo",
"persona": "温柔知性的时尚博主",
"auto_reply_enabled": False,
"schedule_enabled": False,
}
class ConfigManager:
"""配置管理器 - 单例模式"""
_instance = None
_config = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._config is None:
self._config = self._load()
def _load(self) -> dict:
"""从文件加载配置,缺失项用默认值填充"""
config = DEFAULT_CONFIG.copy()
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
saved = json.load(f)
config.update(saved)
except (json.JSONDecodeError, IOError) as e:
logger.warning("配置文件读取失败,使用默认值: %s", e)
return config
def save(self):
"""保存配置到文件"""
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(self._config, f, indent=4, ensure_ascii=False)
except IOError as e:
logger.error("配置保存失败: %s", e)
def get(self, key: str, default=None):
"""获取配置项"""
return self._config.get(key, default)
def set(self, key: str, value):
"""设置配置项并自动保存"""
self._config[key] = value
self.save()
def update(self, data: dict):
"""批量更新配置"""
self._config.update(data)
self.save()
@property
def all(self) -> dict:
"""返回全部配置(副本)"""
return self._config.copy()
def ensure_workspace(self):
"""确保工作空间目录存在"""
os.makedirs(OUTPUT_DIR, exist_ok=True)

223
llm_service.py Normal file
View File

@ -0,0 +1,223 @@
"""
LLM 服务模块
封装对 OpenAI 兼容 API 的调用包含文案生成热点分析评论回复等 Prompt
"""
import requests
import json
import re
import logging
logger = logging.getLogger(__name__)
# ================= Prompt 模板 =================
PROMPT_COPYWRITING = """
你是一个小红书爆款内容专家请根据用户主题生成内容
标题规则(严格执行)
1. 长度限制必须控制在 18 字以内含Emoji绝对不能超过 20
2. 格式要求Emoji + 爆点关键词 + 核心痛点
3. 禁忌禁止使用"第一""""顶级"等绝对化广告法违禁词
4. 风格二极管标题震惊/后悔/必看/避雷/哭了具有强烈的点击欲望
正文规则
1. 口语化多用Emoji分段清晰不堆砌长句
2. 正文控制在 600 字以内小红书限制 1000
3. 结尾必须有 5 个以上相关话题标签(#)。
绘图 Prompt
生成对应的 Stable Diffusion 英文提示词强调masterpiece, best quality, 8k, soft lighting, ins style
返回 JSON 格式
{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}
"""
PROMPT_HOTSPOT_ANALYSIS = """
你是一个小红书运营数据分析专家下面是搜索到的热门笔记信息
{feed_data}
请分析这些热门笔记总结以下内容
1. **热门选题方向**提炼 3-5 个最火的细分选题
2. **标题套路**总结高赞标题的共同特征和写作模板
3. **内容结构**分析爆款笔记的内容组织方式
4. **推荐模仿方案**基于分析结果给出 3 个具体的模仿选题建议
返回 JSON 格式
{{"hot_topics": ["...", "..."], "title_patterns": ["...", "..."], "content_structure": "...", "suggestions": [{{"topic": "...", "reason": "..."}}]}}
"""
PROMPT_COMMENT_REPLY = """
你是一个小红书博主人设为{persona}
有人在你的笔记下评论了请你用符合人设的口吻回复
规则
1. 回复简洁控制在 50 字以内
2. 语气亲切自然像和朋友聊天
3. 适当加入 1-2 Emoji
4. 如果是质疑礼貌回应如果是夸奖真诚感谢
笔记标题{post_title}
用户评论{comment}
直接返回回复内容不需要 JSON 格式
"""
PROMPT_PROACTIVE_COMMENT = """
你是一个小红书活跃用户人设为{persona}
你正在浏览一篇笔记想要留下一条真诚有价值的评论以提升互动和曝光
笔记信息
标题{post_title}
正文摘要{post_content}
已有评论参考可能为空
{existing_comments}
评论规则
1. 评论简洁自然控制在 30-80 不要像机器人
2. 体现你对笔记内容的真实感受或个人经验
3. 可以提问分享类似经历或表达共鸣
4. 适当加入 1-2 Emoji不要过多
5. 不要重复已有评论的观点找新角度
6. 不要生硬带货或自我推广
7. 语气因内容而异教程类请教/补充种草类分享体验生活类表达共鸣
直接返回评论内容不需要 JSON 格式
"""
PROMPT_COPY_WITH_REFERENCE = """
你是一个小红书爆款内容专家参考以下热门笔记的风格和结构创作全新原创内容
参考笔记
{reference_notes}
创作主题{topic}
风格要求{style}
标题规则
1. 长度限制必须控制在 18 字以内含Emoji绝对不能超过 20
2. 借鉴参考笔记的标题套路但内容必须原创
正文规则
1. 口语化多用Emoji分段清晰
2. 正文控制在 600 字以内
3. 结尾有 5 个以上话题标签(#)。
绘图 Prompt
生成 Stable Diffusion 英文提示词
返回 JSON 格式
{{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}}
"""
class LLMService:
"""LLM API 服务封装"""
def __init__(self, api_key: str, base_url: str, model: str = "gpt-3.5-turbo"):
self.api_key = api_key
self.base_url = base_url.rstrip("/")
self.model = model
def _chat(self, system_prompt: str, user_message: str,
json_mode: bool = True, temperature: float = 0.8) -> str:
"""底层聊天接口"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
payload = {
"model": self.model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
"temperature": temperature,
}
if json_mode:
payload["response_format"] = {"type": "json_object"}
try:
resp = requests.post(
f"{self.base_url}/chat/completions",
headers=headers, json=payload, timeout=90
)
resp.raise_for_status()
content = resp.json()["choices"][0]["message"]["content"]
return content
except requests.exceptions.Timeout:
raise TimeoutError("LLM 请求超时,请检查网络或换一个模型")
except requests.exceptions.HTTPError as e:
raise ConnectionError(f"LLM API 错误 ({resp.status_code}): {resp.text[:200]}")
except Exception as e:
raise RuntimeError(f"LLM 调用异常: {e}")
def _parse_json(self, text: str) -> dict:
"""从 LLM 返回文本中解析 JSON"""
cleaned = re.sub(r"```json\s*|```", "", text).strip()
return json.loads(cleaned)
# ---------- 业务方法 ----------
def get_models(self) -> list[str]:
"""获取可用模型列表"""
url = f"{self.base_url}/models"
headers = {"Authorization": f"Bearer {self.api_key}"}
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
data = resp.json()
return [item["id"] for item in data.get("data", [])]
def generate_copy(self, topic: str, style: str) -> dict:
"""生成小红书文案"""
content = self._chat(
PROMPT_COPYWRITING,
f"主题:{topic}\n风格:{style}"
)
data = self._parse_json(content)
# 强制标题长度限制
title = data.get("title", "")
if len(title) > 20:
title = title[:20]
data["title"] = title
return data
def generate_copy_with_reference(self, topic: str, style: str,
reference_notes: str) -> dict:
"""参考热门笔记生成文案"""
prompt = PROMPT_COPY_WITH_REFERENCE.format(
reference_notes=reference_notes, topic=topic, style=style
)
content = self._chat(prompt, f"请创作关于「{topic}」的小红书笔记")
data = self._parse_json(content)
title = data.get("title", "")
if len(title) > 20:
data["title"] = title[:20]
return data
def analyze_hotspots(self, feed_data: str) -> dict:
"""分析热门内容趋势"""
prompt = PROMPT_HOTSPOT_ANALYSIS.format(feed_data=feed_data)
content = self._chat(prompt, "请分析以上热门笔记数据")
return self._parse_json(content)
def generate_reply(self, persona: str, post_title: str, comment: str) -> str:
"""AI 生成评论回复"""
prompt = PROMPT_COMMENT_REPLY.format(
persona=persona, post_title=post_title, comment=comment
)
return self._chat(prompt, "请生成回复", json_mode=False, temperature=0.9).strip()
def generate_proactive_comment(self, persona: str, post_title: str,
post_content: str, existing_comments: str = "") -> str:
"""AI 生成主动评论"""
prompt = PROMPT_PROACTIVE_COMMENT.format(
persona=persona, post_title=post_title,
post_content=post_content,
existing_comments=existing_comments or "暂无评论",
)
return self._chat(prompt, "请生成评论", json_mode=False, temperature=0.9).strip()

1131
main.py Normal file

File diff suppressed because it is too large Load Diff

264
main_v1_backup.py Normal file
View File

@ -0,0 +1,264 @@
import gradio as gr
import requests
import json
import base64
import io
import os
import time
import re
import shutil
import platform
import subprocess
from PIL import Image
# ================= 0. 基础配置与工具 =================
# 强制不走代理连接本地 SD
os.environ['NO_PROXY'] = '127.0.0.1,localhost'
CONFIG_FILE = "config.json"
OUTPUT_DIR = "xhs_workspace"
os.makedirs(OUTPUT_DIR, exist_ok=True)
class ConfigManager:
@staticmethod
def load():
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
pass
return {
"api_key": "",
"base_url": "https://api.openai.com/v1",
"sd_url": "http://127.0.0.1:7860",
"model": "gpt-3.5-turbo"
}
@staticmethod
def save(config_data):
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config_data, f, indent=4, ensure_ascii=False)
# ================= 1. 核心逻辑功能 =================
def get_llm_models(api_key, base_url):
if not api_key or not base_url:
return gr.update(choices=[]), "⚠️ 请先填写配置"
try:
url = f"{base_url.rstrip('/')}/models"
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
data = response.json()
models = [item['id'] for item in data.get('data', [])]
# 保存配置
cfg = ConfigManager.load()
cfg['api_key'] = api_key
cfg['base_url'] = base_url
ConfigManager.save(cfg)
# 修复警告:允许自定义值
return gr.update(choices=models, value=models[0] if models else None), f"✅ 已连接,加载 {len(models)} 个模型"
return gr.update(), f"❌ 连接失败: {response.status_code}"
except Exception as e:
return gr.update(), f"❌ 错误: {e}"
def generate_copy(api_key, base_url, model, topic, style):
if not api_key: return "", "", "", "❌ 缺 API Key"
# --- 核心修改:优化了 Prompt增加字数和违禁词限制 ---
system_prompt = """
你是一个小红书爆款内容专家请根据用户主题生成内容
标题规则(严格执行)
1. 长度限制必须控制在 18 字以内含Emoji绝对不能超过 20
2. 格式要求Emoji + 爆点关键词 + 核心痛点
3. 禁忌禁止使用第一顶级等绝对化广告法违禁词
4. 风格二极管标题震惊/后悔/必看/避雷/哭了具有强烈的点击欲望
正文规则
1. 口语化多用Emoji分段清晰不堆砌长句
2. 结尾必须有 5 个以上相关话题标签(#)。
绘图 Prompt
生成对应的 Stable Diffusion 英文提示词强调masterpiece, best quality, 8k, soft lighting, ins style
返回 JSON 格式
{"title": "...", "content": "...", "sd_prompt": "..."}
"""
try:
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"主题:{topic}\n风格:{style}"}
],
"response_format": {"type": "json_object"}
}
resp = requests.post(f"{base_url.rstrip('/')}/chat/completions", headers=headers, json=payload, timeout=60)
content = resp.json()['choices'][0]['message']['content']
content = re.sub(r'```json\s*|```', '', content).strip()
data = json.loads(content)
# --- 双重保险Python 强制截断 ---
title = data.get('title', '')
# 如果 LLM 不听话超过了20字强制截断并保留前19个字+省略号或者直接保留前20个
if len(title) > 20:
title = title[:20]
return title, data.get('content', ''), data.get('sd_prompt', ''), "✅ 文案生成完毕"
except Exception as e:
return "", "", "", f"❌ 生成失败: {e}"
def get_sd_models(sd_url):
try:
resp = requests.get(f"{sd_url}/sdapi/v1/sd-models", timeout=3)
if resp.status_code == 200:
models = [m['title'] for m in resp.json()]
return gr.update(choices=models, value=models[0] if models else None), "✅ SD 已连接"
return gr.update(choices=[]), "❌ SD 连接失败"
except:
return gr.update(choices=[]), "❌ SD 未启动或端口错误"
def generate_images(sd_url, prompt, neg_prompt, model, steps, cfg):
if not model: return None, "❌ 未选择模型"
# 切换模型
try:
requests.post(f"{sd_url}/sdapi/v1/options", json={"sd_model_checkpoint": model})
except:
pass # 忽略切换错误,继续尝试生成
payload = {
"prompt": prompt,
"negative_prompt": neg_prompt,
"steps": steps,
"cfg_scale": cfg,
"width": 768,
"height": 1024,
"batch_size": 2
}
try:
resp = requests.post(f"{sd_url}/sdapi/v1/txt2img", json=payload, timeout=120)
images = []
for i in resp.json()['images']:
img = Image.open(io.BytesIO(base64.b64decode(i)))
images.append(img)
return images, "✅ 图片生成完毕"
except Exception as e:
return None, f"❌ 绘图失败: {e}"
def one_click_export(title, content, images):
if not title: return "❌ 无法导出:没有标题"
safe_title = re.sub(r'[\\/*?:"<>|]', "", title)[:20]
folder_name = f"{int(time.time())}_{safe_title}"
folder_path = os.path.join(OUTPUT_DIR, folder_name)
os.makedirs(folder_path, exist_ok=True)
with open(os.path.join(folder_path, "文案.txt"), "w", encoding="utf-8") as f:
f.write(f"{title}\n\n{content}")
if images:
for idx, img in enumerate(images):
img.save(os.path.join(folder_path, f"{idx+1}.png"))
try:
if platform.system() == "Windows":
os.startfile(folder_path)
elif platform.system() == "Darwin":
subprocess.call(["open", folder_path])
else:
subprocess.call(["xdg-open", folder_path])
return f"✅ 已导出至: {folder_path}"
except:
return f"✅ 已导出: {folder_path}"
# ================= 2. UI 界面构建 =================
cfg = ConfigManager.load()
with gr.Blocks(title="小红书全自动工作台", theme=gr.themes.Soft()) as app:
gr.Markdown("## 🍒 小红书 AI 爆文生产工坊")
state_images = gr.State([])
with gr.Row():
with gr.Column(scale=1):
with gr.Accordion("⚙️ 系统设置 (自动保存)", open=True):
api_key = gr.Textbox(label="LLM API Key", value=cfg['api_key'], type="password")
base_url = gr.Textbox(label="Base URL", value=cfg['base_url'])
sd_url = gr.Textbox(label="SD URL", value=cfg['sd_url'])
with gr.Row():
btn_connect = gr.Button("🔗 连接并获取模型", size="sm")
btn_refresh_sd = gr.Button("🔄 刷新 SD", size="sm")
# 修复点 1允许自定义值防止报错
llm_model = gr.Dropdown(label="选择 LLM 模型", value=cfg['model'], allow_custom_value=True, interactive=True)
sd_model = gr.Dropdown(label="选择 SD 模型", allow_custom_value=True, interactive=True)
status_bar = gr.Markdown("等待就绪...")
gr.Markdown("### 💡 内容构思")
topic = gr.Textbox(label="笔记主题", placeholder="例如:优衣库早春穿搭")
style = gr.Dropdown(["好物种草", "干货教程", "情绪共鸣", "生活Vlog"], label="风格", value="好物种草")
btn_step1 = gr.Button("✨ 第一步:生成文案方案", variant="primary")
with gr.Column(scale=1):
gr.Markdown("### 📝 文案确认")
# 修复点 2去掉了 show_copy_button 参数,兼容旧版 Gradio
res_title = gr.Textbox(label="标题 (AI生成)", interactive=True)
res_content = gr.TextArea(label="正文 (AI生成)", lines=10, interactive=True)
res_prompt = gr.TextArea(label="绘图提示词", lines=4, interactive=True)
with gr.Accordion("🎨 绘图参数", open=False):
neg_prompt = gr.Textbox(label="反向词", value="nsfw, lowres, bad anatomy, text, error")
steps = gr.Slider(15, 50, value=25, label="步数")
cfg_scale = gr.Slider(1, 15, value=7, label="相关性 (CFG)")
btn_step2 = gr.Button("🎨 第二步:开始绘图", variant="primary")
with gr.Column(scale=1):
gr.Markdown("### 🖼️ 视觉结果")
gallery = gr.Gallery(label="生成预览", columns=1, height="auto")
btn_export = gr.Button("📂 一键导出 (文案+图片)", variant="stop")
export_msg = gr.Markdown("")
# ================= 3. 事件绑定 =================
btn_connect.click(fn=get_llm_models, inputs=[api_key, base_url], outputs=[llm_model, status_bar])
btn_refresh_sd.click(fn=get_sd_models, inputs=[sd_url], outputs=[sd_model, status_bar])
btn_step1.click(
fn=generate_copy,
inputs=[api_key, base_url, llm_model, topic, style],
outputs=[res_title, res_content, res_prompt, status_bar]
)
def on_img_gen(sd_url, p, np, m, s, c):
imgs, msg = generate_images(sd_url, p, np, m, s, c)
return imgs, imgs, msg
btn_step2.click(
fn=on_img_gen,
inputs=[sd_url, res_prompt, neg_prompt, sd_model, steps, cfg_scale],
outputs=[gallery, state_images, status_bar]
)
btn_export.click(
fn=one_click_export,
inputs=[res_title, res_content, state_images],
outputs=[export_msg]
)
app.load(fn=get_sd_models, inputs=[sd_url], outputs=[sd_model, status_bar])
if __name__ == "__main__":
app.launch(inbrowser=True)

857
mcp.md Normal file
View File

@ -0,0 +1,857 @@
# xiaohongshu-mcp
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-21-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![善款已捐](https://img.shields.io/badge/善款已捐-CNY%201300.00-brightgreen?style=flat-square)](./DONATIONS.md)
[![爱心汇聚](https://img.shields.io/badge/爱心汇聚-CNY%20969.95-blue?style=flat-square)](./DONATIONS.md)
[![Docker Pulls](https://img.shields.io/docker/pulls/xpzouying/xiaohongshu-mcp?style=flat-square&logo=docker)](https://hub.docker.com/r/xpzouying/xiaohongshu-mcp)
MCP for 小红书/xiaohongshu.com。
- 我的博客文章:[haha.ai/xiaohongshu-mcp](https://www.haha.ai/xiaohongshu-mcp)
**遇到任何问题,务必要先看 [各种疑难杂症](https://github.com/xpzouying/xiaohongshu-mcp/issues/56)**。
上面的 **疑难杂症** 列表后,还是解决不了你的部署问题,那么强烈推荐使用我写的另外一个工具:[xpzouying/x-mcp](https://github.com/xpzouying/x-mcp),这个工具不需要你进行部署,只需要通过浏览器插件就能驱动你的 MCP对于非技术同学来说更加友好。
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=xpzouying/xiaohongshu-mcp&type=Timeline)](https://www.star-history.com/#xpzouying/xiaohongshu-mcp&Timeline)
## 赞赏支持
本项目所有的赞赏都会用于慈善捐赠。所有的慈善捐赠记录,请参考 [DONATIONS.md](./DONATIONS.md)。
**捐赠时,请备注 MCP 以及名字。**
如需更正/撤回署名,请开 Issue 或通过邮箱联系。
**支付宝(不展示二维码):**
通过支付宝向 **xpzouying@gmail.com** 赞赏。
**微信:**
<img src="donate/wechat@2x.png" alt="WeChat Pay QR" width="260" />
## 项目简介
**主要功能**
> 💡 **提示:** 点击下方功能标题可展开查看视频演示
<details>
<summary><b>1. 登录和检查登录状态</b></summary>
第一步必须,小红书需要进行登录。可以检查当前登录状态。
**登录演示:**
https://github.com/user-attachments/assets/8b05eb42-d437-41b7-9235-e2143f19e8b7
**检查登录状态演示:**
https://github.com/user-attachments/assets/bd9a9a4a-58cb-4421-b8f3-015f703ce1f9
</details>
<details>
<summary><b>2. 发布图文内容</b></summary>
支持发布图文内容到小红书,包括标题、内容描述和图片。
**图片支持方式:**
支持两种图片输入方式:
1. **HTTP/HTTPS 图片链接**
```
["https://example.com/image1.jpg", "https://example.com/image2.png"]
```
2. **本地图片绝对路径**(推荐)
```
["/Users/username/Pictures/image1.jpg", "/home/user/images/image2.png"]
```
**为什么推荐使用本地路径:**
- ✅ 稳定性更好,不依赖网络
- ✅ 上传速度更快
- ✅ 避免图片链接失效问题
- ✅ 支持更多图片格式
**发布图文帖子演示:**
https://github.com/user-attachments/assets/8aee0814-eb96-40af-b871-e66e6bbb6b06
</details>
<details>
<summary><b>3. 发布视频内容</b></summary>
支持发布视频内容到小红书,包括标题、内容描述和本地视频文件。
**视频支持方式:**
仅支持本地视频文件绝对路径:
```
"/Users/username/Videos/video.mp4"
```
**功能特点:**
- ✅ 支持本地视频文件上传
- ✅ 自动处理视频格式转换
- ✅ 支持标题、内容描述和标签
- ✅ 等待视频处理完成后自动发布
**注意事项:**
- 仅支持本地视频文件,不支持 HTTP 链接
- 视频处理时间较长,请耐心等待
- 建议视频文件大小不超过 1GB
</details>
<details>
<summary><b>4. 搜索内容</b></summary>
根据关键词搜索小红书内容。
**搜索帖子演示:**
https://github.com/user-attachments/assets/03c5077d-6160-4b18-b629-2e40933a1fd3
</details>
<details>
<summary><b>5. 获取推荐列表</b></summary>
获取小红书首页推荐内容列表。
**获取推荐列表演示:**
https://github.com/user-attachments/assets/110fc15d-46f2-4cca-bdad-9de5b5b8cc28
</details>
<details>
<summary><b>6. 获取帖子详情(包括互动数据和评论)</b></summary>
获取小红书帖子的完整详情,包括:
- 帖子内容(标题、描述、图片等)
- 用户信息
- 互动数据(点赞、收藏、分享、评论数)
- 评论列表及子评论
**⚠️ 重要提示:**
- 需要提供帖子 ID 和 xsec_token两个参数缺一不可
- 这两个参数可以从 Feed 列表或搜索结果中获取
- 必须先登录才能使用此功能
**获取帖子详情演示:**
https://github.com/user-attachments/assets/76a26130-a216-4371-a6b3-937b8fda092a
</details>
<details>
<summary><b>7. 发表评论到帖子</b></summary>
支持自动发表评论到小红书帖子。
**功能说明:**
- 自动定位评论输入框
- 输入评论内容并发布
- 支持 HTTP API 和 MCP 工具调用
**⚠️ 重要提示:**
- 需要先登录才能使用此功能
- 需要提供帖子 ID、xsec_token 和评论内容
- 这些参数可以从 Feed 列表或搜索结果中获取
**发表评论演示:**
https://github.com/user-attachments/assets/cc385b6c-422c-489b-a5fc-63e92c695b80
</details>
<details>
<summary><b>8. 获取用户个人主页</b></summary>
获取小红书用户的个人主页信息,包括用户基本信息和笔记内容。
**功能说明:**
- 获取用户基本信息(昵称、简介、头像等)
- 获取关注数、粉丝数、获赞量统计
- 获取用户发布的笔记内容列表
- 支持 HTTP API 和 MCP 工具调用
**⚠️ 重要提示:**
- 需要先登录才能使用此功能
- 需要提供用户 ID 和 xsec_token
- 这些参数可以从 Feed 列表或搜索结果中获取
**返回信息包括:**
- 用户基本信息:昵称、简介、头像、认证状态
- 统计数据:关注数、粉丝数、获赞量、笔记数
- 笔记列表:用户发布的所有公开笔记
</details>
**小红书基础运营知识**
- **标题:(非常重要)小红书要求标题不超过 20 个字**
- **正文:(非常重要):正文不能超过 1000 个字**
- 当前支持图文发送以及视频发送:从推荐的角度看,图文的流量会比视频以及纯文字的更好。
- 低优先级可以考虑纯文字的支持。1. 个人感觉纯文字会大大增加运营的复杂度2. 纯文字在我的使用场景的价值较低。
- Tags现已支持。添加合适的 Tags 能带来更多的流量。
- 根据本人实操,小红书每天的发帖量应该是 **50 篇**
- **(非常重要)小红书的同一个账号不允许在多个网页端登录**,如果你登录了当前 xiaohongshu-mcp 后,就不要再在其他的网页端登录该账号,否则就会把当前 MCP 的账号“踢出登录”。你可以使用移动 App 端进行查看当前账号信息。
**风险说明**
1. 该项目是在自己的另外一个项目的基础上开源出来的,原来的项目稳定运行一年多,没有出现过封号的情况,只有出现过 Cookies 过期需要重新登录。
2. 我是使用 Claude Code 接入,稳定自动化运营数周后,验证没有问题后开源。
该项目是基于学习的目的,禁止一切违法行为。
**实操结果**
第一天点赞/收藏数达到了 999+
<img width="386" height="278" alt="CleanShot 2025-09-05 at 01 31 55@2x" src="https://github.com/user-attachments/assets/4b5a283b-bd38-45b8-b608-8f818997366c" />
<img width="350" height="280" alt="CleanShot 2025-09-05 at 01 32 49@2x" src="https://github.com/user-attachments/assets/4481e1e7-3ef6-4bbd-8483-dcee8f77a8f2" />
一周左右的成果
<img width="1840" height="582" alt="CleanShot 2025-09-05 at 01 33 13@2x" src="https://github.com/user-attachments/assets/fb367944-dc48-4bbd-8ece-934caa86323e" />
## 1. 使用教程
### 1.1. 快速开始(推荐)
**方式一:下载预编译二进制文件**
直接从 [GitHub Releases](https://github.com/xpzouying/xiaohongshu-mcp/releases) 下载对应平台的二进制文件:
**主程序MCP 服务):**
- **macOS Apple Silicon**: `xiaohongshu-mcp-darwin-arm64`
- **macOS Intel**: `xiaohongshu-mcp-darwin-amd64`
- **Windows x64**: `xiaohongshu-mcp-windows-amd64.exe`
- **Linux x64**: `xiaohongshu-mcp-linux-amd64`
**登录工具:**
- **macOS Apple Silicon**: `xiaohongshu-login-darwin-arm64`
- **macOS Intel**: `xiaohongshu-login-darwin-amd64`
- **Windows x64**: `xiaohongshu-login-windows-amd64.exe`
- **Linux x64**: `xiaohongshu-login-linux-amd64`
使用步骤:
```bash
# 1. 首先运行登录工具
chmod +x xiaohongshu-login-darwin-arm64
./xiaohongshu-login-darwin-arm64
# 2. 然后启动 MCP 服务
chmod +x xiaohongshu-mcp-darwin-arm64
./xiaohongshu-mcp-darwin-arm64
```
**⚠️ 重要提示**:首次运行时会自动下载无头浏览器(约 150MB请确保网络连接正常。后续运行无需重复下载。
**方式二:源码编译**
<details>
<summary>源码编译安装详情</summary>
依赖 Golang 环境,安装方法请参考 [Golang 官方文档](https://go.dev/doc/install)。
设置 Go 国内源的代理,
```bash
# 配置 GOPROXY 环境变量,以下三选一
# 1. 七牛 CDN
go env -w GOPROXY=https://goproxy.cn,direct
# 2. 阿里云
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
# 3. 官方
go env -w GOPROXY=https://goproxy.io,direct
```
</details>
**方式三:使用 Docker 容器(最简单)**
<details>
<summary>Docker 部署详情</summary>
使用 Docker 部署是最简单的方式,无需安装任何开发环境。
**1. 从 Docker Hub 拉取镜像(推荐)**
我们提供了预构建的 Docker 镜像,可以直接从 Docker Hub 拉取使用:
```bash
# 拉取最新镜像
docker pull xpzouying/xiaohongshu-mcp
```
Docker Hub 地址:[https://hub.docker.com/r/xpzouying/xiaohongshu-mcp](https://hub.docker.com/r/xpzouying/xiaohongshu-mcp)
**2. 使用 Docker Compose 启动(推荐)**
我们提供了配置好的 `docker-compose.yml` 文件,可以直接使用:
```bash
# 下载 docker-compose.yml
wget https://raw.githubusercontent.com/xpzouying/xiaohongshu-mcp/main/docker/docker-compose.yml
# 或者如果已经克隆了项目,进入 docker 目录
cd docker
# 启动服务
docker compose up -d
# 查看日志
docker compose logs -f
# 停止服务
docker compose stop
```
**3. 自己构建镜像(可选)**
```bash
# 在项目根目录运行
docker build -t xpzouying/xiaohongshu-mcp .
```
**4. 配置说明**
Docker 版本会自动:
- 配置 Chrome 浏览器和中文字体
- 挂载 `./data` 用于存储 cookies
- 挂载 `./images` 用于存储发布的图片
- 暴露 18060 端口供 MCP 连接
详细使用说明请参考:[Docker 部署指南](./docker/README.md)
</details>
Windows 遇到问题首先看这里:[Windows 安装指南](./docs/windows_guide.md)
### 1.2. 登录
第一次需要手动登录,需要保存小红书的登录状态。
**使用二进制文件**
```bash
# 运行对应平台的登录工具
./xiaohongshu-login-darwin-arm64
```
**使用源码**
```bash
go run cmd/login/main.go
```
### 1.3. 启动 MCP 服务
启动 xiaohongshu-mcp 服务。
**使用二进制文件**
```bash
# 默认:无头模式,没有浏览器界面
./xiaohongshu-mcp-darwin-arm64
# 非无头模式,有浏览器界面
./xiaohongshu-mcp-darwin-arm64 -headless=false
```
**使用源码**
```bash
# 默认:无头模式,没有浏览器界面
go run .
# 非无头模式,有浏览器界面
go run . -headless=false
```
## 1.4. 验证 MCP
```bash
npx @modelcontextprotocol/inspector
```
![运行 Inspector](./assets/run_inspect.png)
运行后,打开红色标记的链接,配置 MCP inspector输入 `http://localhost:18060/mcp` ,点击 `Connect` 按钮。
![配置 MCP inspector](./assets/inspect_mcp.png)
按照上面配置 MCP inspector 后,点击 `List Tools` 按钮,查看所有的 Tools。
## 1.5. 使用 MCP 发布
### 检查登录状态
![检查登录状态](./assets/check_login.gif)
### 发布图文
示例中是从 https://unsplash.com/ 中随机找了个图片做测试。
![发布图文](./assets/inspect_mcp_publish.gif)
### 搜索内容
使用搜索功能,根据关键词搜索小红书内容:
![搜索内容](./assets/search_result.png)
## 2. MCP 客户端接入
本服务支持标准的 Model Context Protocol (MCP),可以接入各种支持 MCP 的 AI 客户端。
### 2.1. 快速开始
#### 启动 MCP 服务
```bash
# 启动服务(默认无头模式)
go run .
# 或者有界面模式
go run . -headless=false
```
服务将运行在:`http://localhost:18060/mcp`
#### 验证服务状态
```bash
# 测试 MCP 连接
curl -X POST http://localhost:18060/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}'
```
#### Claude Code CLI 接入
```bash
# 添加 HTTP MCP 服务器
claude mcp add --transport http xiaohongshu-mcp http://localhost:18060/mcp
# 检查 MCP 是否添加成功(确保 MCP 已经启动的前提下,运行下面命令)
claude mcp list
```
### 2.2. 支持的客户端
<details>
<summary><b>Claude Code CLI</b></summary>
官方命令行工具,已在上面快速开始部分展示:
```bash
# 添加 HTTP MCP 服务器
claude mcp add --transport http xiaohongshu-mcp http://localhost:18060/mcp
# 检查 MCP 是否添加成功(确保 MCP 已经启动的前提下,运行下面命令)
claude mcp list
```
</details>
<details>
<summary><b>Cursor</b></summary>
#### 配置文件的方式
创建或编辑 MCP 配置文件:
**项目级配置**(推荐):
在项目根目录创建 `.cursor/mcp.json`
```json
{
"mcpServers": {
"xiaohongshu-mcp": {
"url": "http://localhost:18060/mcp",
"description": "小红书内容发布服务 - MCP Streamable HTTP"
}
}
}
```
**全局配置**
在用户目录创建 `~/.cursor/mcp.json` (同样内容)。
#### 使用步骤
1. 确保小红书 MCP 服务正在运行
2. 保存配置文件后,重启 Cursor
3. 在 Cursor 聊天中,工具应该自动可用
4. 可以通过聊天界面的 "Available Tools" 查看已连接的 MCP 工具
**Demo**
插件 MCP 接入:
![cursor_mcp_settings](./assets/cursor_mcp_settings.png)
调用 MCP 工具:(以检查登录状态为例)
![cursor_mcp_check_login](./assets/cursor_mcp_check_login.png)
</details>
<details>
<summary><b>VSCode</b></summary>
#### 方法一:使用命令面板配置
1. 按 `Ctrl/Cmd + Shift + P` 打开命令面板
2. 运行 `MCP: Add Server` 命令
3. 选择 `HTTP` 方式。
4. 输入地址: `http://localhost:18060/mcp`,或者修改成对应的 Server 地址。
5. 输入 MCP 名字: `xiaohongshu-mcp`
#### 方法二:直接编辑配置文件
**工作区配置**(推荐):
在项目根目录创建 `.vscode/mcp.json`
```json
{
"servers": {
"xiaohongshu-mcp": {
"url": "http://localhost:18060/mcp",
"type": "http"
}
},
"inputs": []
}
```
**查看配置**
![vscode_config](./assets/vscode_mcp_config.png)
1. 确认运行状态。
2. 查看 `tools` 是否正确检测。
**Demo**
以搜索帖子内容为例:
![vscode_mcp_search](./assets/vscode_search_demo.png)
</details>
<details>
<summary><b>Google Gemini CLI</b></summary>
`~/.gemini/settings.json` 或项目目录 `.gemini/settings.json` 中配置:
```json
{
"mcpServers": {
"xiaohongshu": {
"httpUrl": "http://localhost:18060/mcp",
"timeout": 30000
}
}
}
```
更多信息请参考 [Gemini CLI MCP 文档](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html)
</details>
<details>
<summary><b>MCP Inspector</b></summary>
调试工具,用于测试 MCP 连接:
```bash
# 启动 MCP Inspector
npx @modelcontextprotocol/inspector
# 在浏览器中连接到http://localhost:18060/mcp
```
使用步骤:
- 使用 MCP Inspector 测试连接
- 测试 Ping Server 功能验证连接
- 检查 List Tools 是否返回 6 个工具
</details>
<details>
<summary><b>Cline</b></summary>
Cline 是一个强大的 AI 编程助手,支持 MCP 协议集成。
#### 配置方法
在 Cline 的 MCP 设置中添加以下配置:
```json
{
"xiaohongshu-mcp": {
"url": "http://localhost:18060/mcp",
"type": "streamableHttp",
"autoApprove": [],
"disabled": false
}
}
```
#### 使用步骤
1. 确保小红书 MCP 服务正在运行(`http://localhost:18060/mcp`
2. 在 Cline 中打开 MCP 设置
3. 添加上述配置到 MCP 服务器列表
4. 保存配置并重启 Cline
5. 在对话中可以直接使用小红书相关功能
#### 配置说明
- `url`: MCP 服务地址
- `type`: 使用 `streamableHttp` 类型以获得更好的性能
- `autoApprove`: 可配置自动批准的工具列表(留空表示手动批准)
- `disabled`: 设置为 `false` 启用此 MCP 服务
#### 使用示例
配置完成后,可以在 Cline 中直接使用自然语言操作小红书:
```
帮我检查小红书登录状态
```
```
帮我发布一篇关于春天的图文到小红书,使用这张图片:/path/to/spring.jpg
```
```
搜索小红书上关于"美食"的内容
```
</details>
<details>
<summary><b>其他支持 HTTP MCP 的客户端</b></summary>
任何支持 HTTP MCP 协议的客户端都可以连接到:`http://localhost:18060/mcp`
基本配置模板:
```json
{
"name": "xiaohongshu-mcp",
"url": "http://localhost:18060/mcp",
"type": "http"
}
```
</details>
### 2.3. 可用 MCP 工具
连接成功后,可使用以下 MCP 工具:
- `check_login_status` - 检查小红书登录状态(无参数)
- `publish_content` - 发布图文内容到小红书必需title, content, images
- `images`: 支持 HTTP 链接或本地绝对路径,推荐使用本地路径
- `publish_with_video` - 发布视频内容到小红书必需title, content, video
- `video`: 仅支持本地视频文件绝对路径
- `list_feeds` - 获取小红书首页推荐列表(无参数)
- `search_feeds` - 搜索小红书内容需要keyword
- `get_feed_detail` - 获取帖子详情需要feed_id, xsec_token
- `post_comment_to_feed` - 发表评论到小红书帖子需要feed_id, xsec_token, content
- `user_profile` - 获取用户个人主页信息需要user_id, xsec_token
### 2.4. 使用示例
使用 Claude Code 发布内容到小红书:
**示例 1使用 HTTP 图片链接**
```
帮我写一篇帖子发布到小红书上,
配图为https://cn.bing.com/th?id=OHR.MaoriRock_EN-US6499689741_UHD.jpg&w=3840
图片是:"纽西兰陶波湖的Ngātoroirangi矿湾毛利岩雕© Joppi/Getty Images"
使用 xiaohongshu-mcp 进行发布。
```
**示例 2使用本地图片路径推荐**
```
帮我写一篇关于春天的帖子发布到小红书上,
使用这些本地图片:
- /Users/username/Pictures/spring_flowers.jpg
- /Users/username/Pictures/cherry_blossom.jpg
使用 xiaohongshu-mcp 进行发布。
```
**示例 3发布视频内容**
```
帮我写一篇关于美食制作的视频发布到小红书上,
使用这个本地视频文件:
- /Users/username/Videos/cooking_tutorial.mp4
使用 xiaohongshu-mcp 的视频发布功能。
```
![claude-cli 进行发布](./assets/claude_push.gif)
**发布结果:**
<img src="./assets/publish_result.jpeg" alt="xiaohongshu-mcp 发布结果" width="300">
### 2.5. 💬 MCP 使用常见问题解答
---
**Q:** 为什么检查登录用户名显示 `xiaghgngshu-mcp`
**A:** 用户名是写死的。
---
**Q:** 显示发布成功后,但实际上没有显示?
**A:** 排查步骤如下:
1. 使用 **非无头模式** 重新发布一次。
2. 更换 **不同的内容** 重新发布。
3. 登录网页版小红书,查看账号是否被 **风控限制网页版发布**
4. 检查 **图片大小** 是否过大。
5. 确认 **图片路径中没有中文字符**
6. 若使用网络图片地址,请确认 **图片链接可正常访问**
---
**Q:** 在设备上运行 MCP 程序出现闪退如何解决?
**A:**
1. 建议 **从源码安装**
2. 或使用 **Docker 安装 xiaohongshu-mcp**,教程参考:
- [使用 Docker 安装 xiaohongshu-mcp](https://github.com/xpzouying/xiaohongshu-mcp#:~:text=%E6%96%B9%E5%BC%8F%E4%B8%89%EF%BC%9A%E4%BD%BF%E7%94%A8%20Docker%20%E5%AE%B9%E5%99%A8%EF%BC%88%E6%9C%80%E7%AE%80%E5%8D%95%EF%BC%89)
- [X-MCP 项目页面](https://github.com/xpzouying/x-mcp/)
---
**Q:** 使用 `http://localhost:18060/mcp` 进行 MCP 验证时提示无法连接?
**A:**
- 在 **Docker 环境** 下,请使用
👉 [http://host.docker.internal:18060/mcp](http://host.docker.internal:18060/mcp)
- 在 **非 Docker 环境** 下,请使用 **本机 IPv4 地址** 访问。
---
## 3. 🌟 实战案例展示 (Community Showcases)
> 💡 **强烈推荐查看**:这些都是社区贡献者的真实使用案例,包含详细的配置步骤和实战经验!
### 📚 完整教程列表
1. **[n8n 完整集成教程](./examples/n8n/README.md)** - 工作流自动化平台集成
2. **[Cherry Studio 完整配置教程](./examples/cherrystudio/README.md)** - AI 客户端完美接入
3. **[Claude Code + Kimi K2 接入教程](./examples/claude-code/claude-code-kimi-k2.md)** - Claude Code 门槛太高,那么就接入 Kimi 国产大模型吧~
4. **[AnythingLLM 完整指南](./examples/anythingLLM/readme.md)** - AnythingLLM 是一款 all-in-one 多模态 AI 客户端,支持 workflow 定义,支持多种大模型和插件扩展。
> 🎯 **提示**: 点击上方链接查看详细的图文教程,快速上手各种集成方案!
>
> 📢 **欢迎贡献**: 如果你有新的集成案例,欢迎提交 PR 分享给社区!
## 4. 小红书 MCP 互助群
**重要:在群里问问题之前,请一定要先仔细看完 README 文档以及查看 Issues。**
<!-- 两列排布:飞书二群 | 微信群 -->
| 【飞书 3 群】:扫码进入 | 【微信群 13 群】:扫码进入 |
| ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| <img src="https://github.com/user-attachments/assets/9a0ec41a-cb65-4f4e-a0f7-31658a49512d" alt="qrcode_2qun" width="300"> | <img src="https://github.com/user-attachments/assets/eb1c4a86-a88c-4ce9-8eb2-db9a855f44e6" alt="WechatIMG119" width="300"> |
## 🙏 致谢贡献者 ✨
感谢以下所有为本项目做出贡献的朋友!(排名不分先后)
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://haha.ai"><img src="https://avatars.githubusercontent.com/u/3946563?v=4?s=100" width="100px;" alt="zy"/><br /><sub><b>zy</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=xpzouying" title="Code">💻</a> <a href="#ideas-xpzouying" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=xpzouying" title="Documentation">📖</a> <a href="#design-xpzouying" title="Design">🎨</a> <a href="#maintenance-xpzouying" title="Maintenance">🚧</a> <a href="#infra-xpzouying" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/xpzouying/xiaohongshu-mcp/pulls?q=is%3Apr+reviewed-by%3Axpzouying" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.hwbuluo.com"><img src="https://avatars.githubusercontent.com/u/1271815?v=4?s=100" width="100px;" alt="clearwater"/><br /><sub><b>clearwater</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=esperyong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/laryzhong"><img src="https://avatars.githubusercontent.com/u/47939471?v=4?s=100" width="100px;" alt="Zhongpeng"/><br /><sub><b>Zhongpeng</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=laryzhong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DTDucas"><img src="https://avatars.githubusercontent.com/u/105262836?v=4?s=100" width="100px;" alt="Duong Tran"/><br /><sub><b>Duong Tran</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=DTDucas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Angiin"><img src="https://avatars.githubusercontent.com/u/17389304?v=4?s=100" width="100px;" alt="Angiin"/><br /><sub><b>Angiin</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=Angiin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/muhenan"><img src="https://avatars.githubusercontent.com/u/43441941?v=4?s=100" width="100px;" alt="Henan Mu"/><br /><sub><b>Henan Mu</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=muhenan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chengazhen"><img src="https://avatars.githubusercontent.com/u/52627267?v=4?s=100" width="100px;" alt="Journey"/><br /><sub><b>Journey</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=chengazhen" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eveyuyi"><img src="https://avatars.githubusercontent.com/u/69026872?v=4?s=100" width="100px;" alt="Eve Yu"/><br /><sub><b>Eve Yu</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=eveyuyi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CooperGuo"><img src="https://avatars.githubusercontent.com/u/183056602?v=4?s=100" width="100px;" alt="CooperGuo"/><br /><sub><b>CooperGuo</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=CooperGuo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://biboyqg.github.io/"><img src="https://avatars.githubusercontent.com/u/125724218?v=4?s=100" width="100px;" alt="Banghao Chi"/><br /><sub><b>Banghao Chi</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=BiboyQG" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/varz1"><img src="https://avatars.githubusercontent.com/u/60377372?v=4?s=100" width="100px;" alt="varz1"/><br /><sub><b>varz1</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=varz1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://google.meloguan.site"><img src="https://avatars.githubusercontent.com/u/62586556?v=4?s=100" width="100px;" alt="Melo Y Guan"/><br /><sub><b>Melo Y Guan</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=Meloyg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lmxdawn"><img src="https://avatars.githubusercontent.com/u/21293193?v=4?s=100" width="100px;" alt="lmxdawn"/><br /><sub><b>lmxdawn</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=lmxdawn" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haikow"><img src="https://avatars.githubusercontent.com/u/22428382?v=4?s=100" width="100px;" alt="haikow"/><br /><sub><b>haikow</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=haikow" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://carlo-blog.aiju.fun/"><img src="https://avatars.githubusercontent.com/u/18513362?v=4?s=100" width="100px;" alt="Carlo"/><br /><sub><b>Carlo</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=a67793581" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hrz394943230"><img src="https://avatars.githubusercontent.com/u/28583005?v=4?s=100" width="100px;" alt="hrz"/><br /><sub><b>hrz</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=hrz394943230" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ctrlz526"><img src="https://avatars.githubusercontent.com/u/143257420?v=4?s=100" width="100px;" alt="Ctrlz"/><br /><sub><b>Ctrlz</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=ctrlz526" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/flippancy"><img src="https://avatars.githubusercontent.com/u/6467703?v=4?s=100" width="100px;" alt="flippancy"/><br /><sub><b>flippancy</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=flippancy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Infinityay"><img src="https://avatars.githubusercontent.com/u/103165980?v=4?s=100" width="100px;" alt="Yuhang Lu"/><br /><sub><b>Yuhang Lu</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=Infinityay" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://triepod.ai"><img src="https://avatars.githubusercontent.com/u/199543909?v=4?s=100" width="100px;" alt="Bryan Thompson"/><br /><sub><b>Bryan Thompson</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=triepod-ai" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.megvii.com"><img src="https://avatars.githubusercontent.com/u/7806992?v=4?s=100" width="100px;" alt="tan jun"/><br /><sub><b>tan jun</b></sub></a><br /><a href="https://github.com/xpzouying/xiaohongshu-mcp/commits?author=tanxxjun321" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
### ✨ 特别感谢
| 贡献者 |
| --------------------------------------------------------------------------------------------------------------------------- |
| [<img src="https://avatars.githubusercontent.com/wanpengxie" width="100px;"><br>@wanpengxie](https://github.com/wanpengxie) |
本项目遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范。欢迎任何形式的贡献!

321
mcp_client.py Normal file
View File

@ -0,0 +1,321 @@
"""
小红书 MCP HTTP 客户端
封装对 xiaohongshu-mcp 服务 (http://localhost:18060/mcp) 的调用
"""
import requests
import json
import logging
import uuid
import base64
import re
import io
from PIL import Image
logger = logging.getLogger(__name__)
MCP_DEFAULT_URL = "http://localhost:18060/mcp"
MCP_TIMEOUT = 60 # 秒
class MCPClient:
"""小红书 MCP 服务的 HTTP 客户端封装"""
def __init__(self, base_url: str = MCP_DEFAULT_URL):
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({"Content-Type": "application/json"})
self._session_id = None
self._initialized = False
# ---------- 底层通信 ----------
def _call(self, method: str, params: dict = None) -> dict:
"""发送 JSON-RPC 请求到 MCP 服务"""
payload = {
"jsonrpc": "2.0",
"method": method,
"params": params or {},
"id": str(uuid.uuid4()),
}
headers = {}
if self._session_id:
headers["mcp-session-id"] = self._session_id
try:
resp = self.session.post(
self.base_url, json=payload, timeout=MCP_TIMEOUT, headers=headers
)
# 保存 session id
if "mcp-session-id" in resp.headers:
self._session_id = resp.headers["mcp-session-id"]
resp.raise_for_status()
data = resp.json()
if "error" in data:
logger.error("MCP error: %s", data["error"])
return {"error": data["error"]}
return data.get("result", data)
except requests.exceptions.ConnectionError:
logger.error("MCP 服务未启动或无法连接: %s", self.base_url)
return {"error": "MCP 服务未启动,请先启动 xiaohongshu-mcp"}
except requests.exceptions.Timeout:
logger.error("MCP 请求超时")
return {"error": "MCP 请求超时,请稍后重试"}
except Exception as e:
logger.error("MCP 调用异常: %s", e)
return {"error": str(e)}
def _ensure_initialized(self):
"""确保 MCP 连接已初始化"""
if not self._initialized:
result = self._call("initialize", {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "xhs-autobot", "version": "2.0.0"}
})
if "error" not in result:
# 发送 initialized 通知
self._call("notifications/initialized", {})
self._initialized = True
return result
return {"status": "already_initialized"}
def _call_tool(self, tool_name: str, arguments: dict = None) -> dict:
"""调用 MCP 工具"""
self._ensure_initialized()
result = self._call("tools/call", {
"name": tool_name,
"arguments": arguments or {}
})
# 提取文本和图片内容
if isinstance(result, dict) and "content" in result:
texts = []
images = []
for item in result["content"]:
if item.get("type") == "text":
texts.append(item["text"])
elif item.get("type") == "image":
# MCP 返回的 base64 图片
img_data = item.get("data", "")
if img_data:
images.append(img_data)
out = {"success": True, "text": "\n".join(texts), "raw": result}
if images:
out["images"] = images
return out
return result
# ---------- 登录 ----------
def get_login_qrcode(self) -> dict:
"""获取登录二维码,返回 {success, text, qr_image(PIL.Image)}"""
result = self._call_tool("get_login_qrcode")
if "error" in result:
return result
# 尝试解析 base64 图片
qr_image = None
if "images" in result and result["images"]:
try:
img_bytes = base64.b64decode(result["images"][0])
qr_image = Image.open(io.BytesIO(img_bytes))
except Exception as e:
logger.warning("二维码图片解析失败: %s", e)
result["qr_image"] = qr_image
return result
def check_login_status(self) -> dict:
"""检查小红书登录状态"""
return self._call_tool("check_login_status")
# ---------- 连接状态 ----------
def check_connection(self) -> tuple[bool, str]:
"""检查 MCP 服务是否可连接"""
result = self._call_tool("check_login_status")
if "error" in result:
return False, result["error"]
return True, result.get("text", "已连接")
# ---------- 搜索 ----------
def search_feeds(self, keyword: str, sort_by: str = "综合",
note_type: str = "不限", publish_time: str = "不限") -> dict:
"""搜索小红书内容"""
args = {
"keyword": keyword,
"filters": {
"sort_by": sort_by,
"note_type": note_type,
"publish_time": publish_time,
}
}
return self._call_tool("search_feeds", args)
# ---------- 推荐列表 ----------
def list_feeds(self) -> dict:
"""获取首页推荐列表"""
return self._call_tool("list_feeds")
# ---------- 笔记列表解析 ----------
@staticmethod
def _parse_feed_entries(text: str) -> list[dict]:
"""从 MCP 返回文本中解析笔记条目为结构化列表"""
entries = []
# 方式1: 尝试直接 JSON 解析
try:
data = json.loads(text)
feeds = []
if isinstance(data, dict) and "feeds" in data:
feeds = data["feeds"]
elif isinstance(data, list):
feeds = data
for feed in feeds:
note = feed.get("noteCard", {})
user = note.get("user", {})
interact = note.get("interactInfo", {})
entries.append({
"feed_id": feed.get("id", ""),
"xsec_token": feed.get("xsecToken", ""),
"title": note.get("displayTitle", "未知标题"),
"author": user.get("nickname", user.get("nickName", "")),
"user_id": user.get("userId", ""),
"likes": interact.get("likedCount", "0"),
"type": note.get("type", ""),
})
if entries:
return entries
except (json.JSONDecodeError, TypeError, AttributeError):
pass
# 方式2: 正则提取 —— 适配 MCP 的文本格式
# 匹配 feed_id (24位十六进制)
feed_ids = re.findall(r'(?:feed_id|id)["\s:]+([0-9a-f]{24})', text, re.I)
# 匹配 xsecToken
tokens = re.findall(r'(?:xsec_?[Tt]oken)["\s:]+([A-Za-z0-9+/=_-]{20,})', text, re.I)
# 匹配标题
titles = re.findall(r'(?:title|标题)["\s:]+(.+?)(?:\n|$)', text, re.I)
# 匹配 userId
user_ids = re.findall(r'(?:user_?[Ii]d|userId)["\s:]+([0-9a-f]{24})', text, re.I)
count = max(len(feed_ids), len(tokens))
for i in range(count):
entries.append({
"feed_id": feed_ids[i] if i < len(feed_ids) else "",
"xsec_token": tokens[i] if i < len(tokens) else "",
"title": titles[i].strip() if i < len(titles) else f"笔记 {i+1}",
"author": "",
"user_id": user_ids[i] if i < len(user_ids) else "",
"likes": "",
"type": "",
})
return entries
def list_feeds_parsed(self) -> list[dict]:
"""获取首页推荐并解析为结构化列表"""
result = self.list_feeds()
if "error" in result:
return []
return self._parse_feed_entries(result.get("text", ""))
def search_feeds_parsed(self, keyword: str, sort_by: str = "综合") -> list[dict]:
"""搜索笔记并解析为结构化列表"""
result = self.search_feeds(keyword, sort_by=sort_by)
if "error" in result:
return []
return self._parse_feed_entries(result.get("text", ""))
# ---------- 帖子详情 ----------
def get_feed_detail(self, feed_id: str, xsec_token: str,
load_all_comments: bool = False) -> dict:
"""获取笔记详情"""
args = {
"feed_id": feed_id,
"xsec_token": xsec_token,
"load_all_comments": load_all_comments,
}
return self._call_tool("get_feed_detail", args)
# ---------- 发布 ----------
def publish_content(self, title: str, content: str, images: list[str],
tags: list[str] = None, schedule_at: str = None) -> dict:
"""发布图文内容"""
args = {
"title": title,
"content": content,
"images": images,
}
if tags:
args["tags"] = tags
if schedule_at:
args["schedule_at"] = schedule_at
return self._call_tool("publish_content", args)
def publish_video(self, title: str, content: str, video_path: str,
tags: list[str] = None, schedule_at: str = None) -> dict:
"""发布视频内容"""
args = {
"title": title,
"content": content,
"video": video_path,
}
if tags:
args["tags"] = tags
if schedule_at:
args["schedule_at"] = schedule_at
return self._call_tool("publish_with_video", args)
# ---------- 评论 ----------
def post_comment(self, feed_id: str, xsec_token: str, comment: str) -> dict:
"""发表评论"""
return self._call_tool("post_comment_to_feed", {
"feed_id": feed_id,
"xsec_token": xsec_token,
"content": comment,
})
def reply_comment(self, feed_id: str, xsec_token: str,
comment_id: str, user_id: str, content: str) -> dict:
"""回复评论"""
return self._call_tool("reply_comment_in_feed", {
"feed_id": feed_id,
"xsec_token": xsec_token,
"comment_id": comment_id,
"user_id": user_id,
"content": content,
})
# ---------- 互动 ----------
def like_feed(self, feed_id: str, xsec_token: str, unlike: bool = False) -> dict:
"""点赞/取消点赞"""
return self._call_tool("like_feed", {
"feed_id": feed_id,
"xsec_token": xsec_token,
"unlike": unlike,
})
def favorite_feed(self, feed_id: str, xsec_token: str,
unfavorite: bool = False) -> dict:
"""收藏/取消收藏"""
return self._call_tool("favorite_feed", {
"feed_id": feed_id,
"xsec_token": xsec_token,
"unfavorite": unfavorite,
})
# ---------- 用户 ----------
def get_user_profile(self, user_id: str, xsec_token: str) -> dict:
"""获取用户主页信息"""
return self._call_tool("user_profile", {
"user_id": user_id,
"xsec_token": xsec_token,
})

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
gradio>=4.0.0
requests>=2.28.0
Pillow>=9.0.0

145
sd_service.py Normal file
View File

@ -0,0 +1,145 @@
"""
Stable Diffusion 服务模块
封装对 SD WebUI API 的调用支持 txt2img img2img
"""
import requests
import base64
import io
import logging
from PIL import Image
logger = logging.getLogger(__name__)
SD_TIMEOUT = 180 # 图片生成可能需要较长时间
# 默认反向提示词
DEFAULT_NEGATIVE = (
"nsfw, lowres, bad anatomy, bad hands, text, error, missing fingers, "
"extra digit, fewer digits, cropped, worst quality, low quality, "
"normal quality, jpeg artifacts, signature, watermark, blurry"
)
class SDService:
"""Stable Diffusion WebUI API 封装"""
def __init__(self, sd_url: str = "http://127.0.0.1:7860"):
self.sd_url = sd_url.rstrip("/")
def check_connection(self) -> tuple[bool, str]:
"""检查 SD 服务是否可用"""
try:
resp = requests.get(f"{self.sd_url}/sdapi/v1/sd-models", timeout=5)
if resp.status_code == 200:
count = len(resp.json())
return True, f"SD 已连接,{count} 个模型可用"
return False, f"SD 返回异常状态: {resp.status_code}"
except requests.exceptions.ConnectionError:
return False, "SD WebUI 未启动或端口错误"
except Exception as e:
return False, f"SD 连接失败: {e}"
def get_models(self) -> list[str]:
"""获取 SD 模型列表"""
resp = requests.get(f"{self.sd_url}/sdapi/v1/sd-models", timeout=5)
resp.raise_for_status()
return [m["title"] for m in resp.json()]
def switch_model(self, model_name: str):
"""切换 SD 模型"""
try:
requests.post(
f"{self.sd_url}/sdapi/v1/options",
json={"sd_model_checkpoint": model_name},
timeout=60,
)
except Exception as e:
logger.warning("模型切换失败: %s", e)
def txt2img(
self,
prompt: str,
negative_prompt: str = DEFAULT_NEGATIVE,
model: str = None,
steps: int = 25,
cfg_scale: float = 7.0,
width: int = 768,
height: int = 1024,
batch_size: int = 2,
seed: int = -1,
) -> list[Image.Image]:
"""文生图"""
if model:
self.switch_model(model)
payload = {
"prompt": prompt,
"negative_prompt": negative_prompt,
"steps": steps,
"cfg_scale": cfg_scale,
"width": width,
"height": height,
"batch_size": batch_size,
"seed": seed,
}
resp = requests.post(
f"{self.sd_url}/sdapi/v1/txt2img",
json=payload,
timeout=SD_TIMEOUT,
)
resp.raise_for_status()
images = []
for img_b64 in resp.json().get("images", []):
img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
images.append(img)
return images
def img2img(
self,
init_image: Image.Image,
prompt: str,
negative_prompt: str = DEFAULT_NEGATIVE,
denoising_strength: float = 0.6,
steps: int = 25,
cfg_scale: float = 7.0,
) -> list[Image.Image]:
"""图生图(参考图修改)"""
# 将 PIL Image 转为 base64
buf = io.BytesIO()
init_image.save(buf, format="PNG")
init_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
payload = {
"init_images": [init_b64],
"prompt": prompt,
"negative_prompt": negative_prompt,
"denoising_strength": denoising_strength,
"steps": steps,
"cfg_scale": cfg_scale,
"width": init_image.width,
"height": init_image.height,
}
resp = requests.post(
f"{self.sd_url}/sdapi/v1/img2img",
json=payload,
timeout=SD_TIMEOUT,
)
resp.raise_for_status()
images = []
for img_b64 in resp.json().get("images", []):
img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
images.append(img)
return images
def get_lora_models(self) -> list[str]:
"""获取可用的 LoRA 模型列表"""
try:
resp = requests.get(f"{self.sd_url}/sdapi/v1/loras", timeout=5)
resp.raise_for_status()
return [lora["name"] for lora in resp.json()]
except Exception:
return []