commit 88faca150d7b914f3c7f64795d89a080c33ceb32
Author: zhoujie <929834232@qq.com>
Date: Sun Feb 8 14:21:50 2026 +0800
✨ 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)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4287568
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+xhs_workspace
+__pycache__
+*.log
\ No newline at end of file
diff --git a/Todo.md b/Todo.md
new file mode 100644
index 0000000..ab1c0a1
--- /dev/null
+++ b/Todo.md
@@ -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 图,想混排自己拍的照片,增加“本地上传”按钮。第三阶段:自动化运营 (解放双手)自动回复机器人:根据设定的人设(知性姐姐/毒舌博主)自动回评论。定时任务:设置一个队列,让它自己跑。
\ No newline at end of file
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..52ab15c
--- /dev/null
+++ b/config.json
@@ -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
+}
\ No newline at end of file
diff --git a/config_manager.py b/config_manager.py
new file mode 100644
index 0000000..fdab8f9
--- /dev/null
+++ b/config_manager.py
@@ -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)
diff --git a/llm_service.py b/llm_service.py
new file mode 100644
index 0000000..eb47973
--- /dev/null
+++ b/llm_service.py
@@ -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()
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..ad70240
--- /dev/null
+++ b/main.py
@@ -0,0 +1,1131 @@
+"""
+小红书 AI 爆文生产工坊 V2.0
+全自动工作台:灵感 -> 文案 -> 绘图 -> 发布 -> 运营
+"""
+import gradio as gr
+import os
+import re
+import time
+import logging
+import platform
+import subprocess
+from PIL import Image
+
+from config_manager import ConfigManager, OUTPUT_DIR
+from llm_service import LLMService
+from sd_service import SDService, DEFAULT_NEGATIVE
+from mcp_client import MCPClient
+
+# ================= 日志配置 =================
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
+ handlers=[
+ logging.StreamHandler(),
+ logging.FileHandler("autobot.log", encoding="utf-8"),
+ ],
+)
+logger = logging.getLogger("autobot")
+
+# 强制不走代理连接本地服务
+os.environ["NO_PROXY"] = "127.0.0.1,localhost"
+
+# ================= 全局服务初始化 =================
+
+cfg = ConfigManager()
+cfg.ensure_workspace()
+
+mcp = MCPClient(cfg.get("mcp_url", "http://localhost:18060/mcp"))
+
+# ==================================================
+# Tab 1: 内容创作
+# ==================================================
+
+
+def connect_llm(api_key, base_url):
+ """连接 LLM 并获取模型列表"""
+ if not api_key or not base_url:
+ return gr.update(choices=[]), "⚠️ 请先填写 API Key 和 Base URL"
+ try:
+ svc = LLMService(api_key, base_url)
+ models = svc.get_models()
+ cfg.update({"api_key": api_key, "base_url": base_url})
+ return (
+ gr.update(choices=models, value=models[0] if models else None),
+ f"✅ 已连接,加载 {len(models)} 个模型",
+ )
+ except Exception as e:
+ logger.error("LLM 连接失败: %s", e)
+ return gr.update(), f"❌ 连接失败: {e}"
+
+
+def connect_sd(sd_url):
+ """连接 SD 并获取模型列表"""
+ try:
+ svc = SDService(sd_url)
+ ok, msg = svc.check_connection()
+ if ok:
+ models = svc.get_models()
+ cfg.set("sd_url", sd_url)
+ return gr.update(choices=models, value=models[0] if models else None), f"✅ {msg}"
+ return gr.update(choices=[]), f"❌ {msg}"
+ except Exception as e:
+ logger.error("SD 连接失败: %s", e)
+ return gr.update(choices=[]), f"❌ SD 连接失败: {e}"
+
+
+def check_mcp_status(mcp_url):
+ """检查 MCP 连接状态"""
+ try:
+ client = MCPClient(mcp_url)
+ ok, msg = client.check_connection()
+ if ok:
+ cfg.set("mcp_url", mcp_url)
+ return f"✅ MCP 服务正常 - {msg}"
+ return f"❌ {msg}"
+ except Exception as e:
+ return f"❌ MCP 连接失败: {e}"
+
+
+# ==================================================
+# 小红书账号登录
+# ==================================================
+
+
+def get_login_qrcode(mcp_url):
+ """获取小红书登录二维码"""
+ try:
+ client = MCPClient(mcp_url)
+ result = client.get_login_qrcode()
+ if "error" in result:
+ return None, f"❌ 获取二维码失败: {result['error']}"
+ qr_image = result.get("qr_image")
+ msg = result.get("text", "")
+ if qr_image:
+ return qr_image, f"✅ 二维码已生成,请用小红书 App 扫码\n{msg}"
+ return None, f"⚠️ 未获取到二维码图片,MCP 返回:\n{msg}"
+ except Exception as e:
+ logger.error("获取登录二维码失败: %s", e)
+ return None, f"❌ 获取二维码失败: {e}"
+
+
+def check_login(mcp_url):
+ """检查小红书登录状态"""
+ try:
+ client = MCPClient(mcp_url)
+ result = client.check_login_status()
+ if "error" in result:
+ return f"❌ {result['error']}"
+ text = result.get("text", "")
+ if "未登录" in text:
+ return f"🔴 {text}"
+ return f"🟢 {text}"
+ except Exception as e:
+ return f"❌ 检查登录状态失败: {e}"
+
+
+def generate_copy(api_key, base_url, model, topic, style):
+ """生成文案"""
+ if not api_key:
+ return "", "", "", "", "❌ 缺少 API Key"
+ try:
+ svc = LLMService(api_key, base_url, model)
+ data = svc.generate_copy(topic, style)
+ cfg.set("model", model)
+ tags = data.get("tags", [])
+ return (
+ data.get("title", ""),
+ data.get("content", ""),
+ data.get("sd_prompt", ""),
+ ", ".join(tags) if tags else "",
+ "✅ 文案生成完毕",
+ )
+ except Exception as e:
+ logger.error("文案生成失败: %s", e)
+ return "", "", "", "", f"❌ 生成失败: {e}"
+
+
+def generate_images(sd_url, prompt, neg_prompt, model, steps, cfg_scale):
+ """生成图片"""
+ if not model:
+ return None, [], "❌ 未选择 SD 模型"
+ try:
+ svc = SDService(sd_url)
+ images = svc.txt2img(
+ prompt=prompt,
+ negative_prompt=neg_prompt,
+ model=model,
+ steps=int(steps),
+ cfg_scale=float(cfg_scale),
+ )
+ return images, images, f"✅ 生成 {len(images)} 张图片"
+ except Exception as e:
+ logger.error("图片生成失败: %s", 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}")
+
+ saved_paths = []
+ if images:
+ for idx, img in enumerate(images):
+ path = os.path.join(folder_path, f"图{idx+1}.png")
+ if isinstance(img, Image.Image):
+ img.save(path)
+ saved_paths.append(os.path.abspath(path))
+
+ # 尝试打开文件夹
+ try:
+ abs_path = os.path.abspath(folder_path)
+ if platform.system() == "Windows":
+ os.startfile(abs_path)
+ elif platform.system() == "Darwin":
+ subprocess.call(["open", abs_path])
+ else:
+ subprocess.call(["xdg-open", abs_path])
+ except Exception:
+ pass
+
+ return f"✅ 已导出至: {folder_path} ({len(saved_paths)} 张图片)"
+
+
+def publish_to_xhs(title, content, tags_str, images, local_images, mcp_url, schedule_time):
+ """通过 MCP 发布到小红书"""
+ if not title:
+ return "❌ 缺少标题"
+
+ client = MCPClient(mcp_url)
+
+ # 收集图片路径
+ image_paths = []
+
+ # 先保存 AI 生成的图片到临时目录
+ if images:
+ temp_dir = os.path.join(OUTPUT_DIR, "_temp_publish")
+ os.makedirs(temp_dir, exist_ok=True)
+ for idx, img in enumerate(images):
+ if isinstance(img, Image.Image):
+ path = os.path.abspath(os.path.join(temp_dir, f"ai_{idx}.png"))
+ img.save(path)
+ image_paths.append(path)
+
+ # 添加本地上传的图片
+ if local_images:
+ for img_file in local_images:
+ # Gradio File 组件返回的是 NamedString 或 tempfile path
+ img_path = img_file.name if hasattr(img_file, 'name') else str(img_file)
+ if os.path.exists(img_path):
+ image_paths.append(os.path.abspath(img_path))
+
+ if not image_paths:
+ return "❌ 至少需要 1 张图片才能发布"
+
+ # 解析标签
+ tags = [t.strip().lstrip("#") for t in tags_str.split(",") if t.strip()] if tags_str else None
+
+ # 定时发布
+ schedule = schedule_time if schedule_time and schedule_time.strip() else None
+
+ try:
+ result = client.publish_content(
+ title=title,
+ content=content,
+ images=image_paths,
+ tags=tags,
+ schedule_at=schedule,
+ )
+ if "error" in result:
+ return f"❌ 发布失败: {result['error']}"
+ return f"✅ 发布成功!\n{result.get('text', '')}"
+ except Exception as e:
+ logger.error("发布失败: %s", e)
+ return f"❌ 发布异常: {e}"
+
+
+# ==================================================
+# Tab 2: 热点探测
+# ==================================================
+
+
+def search_hotspots(keyword, sort_by, mcp_url):
+ """搜索小红书热门内容"""
+ if not keyword:
+ return "❌ 请输入搜索关键词", ""
+ try:
+ client = MCPClient(mcp_url)
+ result = client.search_feeds(keyword, sort_by=sort_by)
+ if "error" in result:
+ return f"❌ 搜索失败: {result['error']}", ""
+ text = result.get("text", "无结果")
+ return "✅ 搜索完成", text
+ except Exception as e:
+ logger.error("热点搜索失败: %s", e)
+ return f"❌ 搜索失败: {e}", ""
+
+
+def analyze_and_suggest(api_key, base_url, model, keyword, search_result):
+ """AI 分析热点并给出建议"""
+ if not search_result:
+ return "❌ 请先搜索", "", ""
+ try:
+ svc = LLMService(api_key, base_url, model)
+ analysis = svc.analyze_hotspots(search_result)
+
+ topics = "\n".join(f"• {t}" for t in analysis.get("hot_topics", []))
+ patterns = "\n".join(f"• {p}" for p in analysis.get("title_patterns", []))
+ suggestions = "\n".join(
+ f"**{s['topic']}** - {s['reason']}"
+ for s in analysis.get("suggestions", [])
+ )
+ structure = analysis.get("content_structure", "")
+
+ summary = (
+ f"## 🔥 热门选题\n{topics}\n\n"
+ f"## 📝 标题套路\n{patterns}\n\n"
+ f"## 📐 内容结构\n{structure}\n\n"
+ f"## 💡 推荐选题\n{suggestions}"
+ )
+ return "✅ 分析完成", summary, keyword
+ except Exception as e:
+ logger.error("热点分析失败: %s", e)
+ return f"❌ 分析失败: {e}", "", ""
+
+
+def generate_from_hotspot(api_key, base_url, model, topic_from_hotspot, style, search_result):
+ """基于热点分析生成文案"""
+ if not topic_from_hotspot:
+ return "", "", "", "", "❌ 请先选择或输入选题"
+ try:
+ svc = LLMService(api_key, base_url, model)
+ data = svc.generate_copy_with_reference(
+ topic=topic_from_hotspot,
+ style=style,
+ reference_notes=search_result[:2000], # 截断防止超长
+ )
+ tags = data.get("tags", [])
+ return (
+ data.get("title", ""),
+ data.get("content", ""),
+ data.get("sd_prompt", ""),
+ ", ".join(tags),
+ "✅ 基于热点的文案已生成",
+ )
+ except Exception as e:
+ return "", "", "", "", f"❌ 生成失败: {e}"
+
+
+# ==================================================
+# Tab 3: 评论管家
+# ==================================================
+
+# ---- 共用: 笔记列表缓存 ----
+
+# 主动评论缓存
+_cached_proactive_entries: list[dict] = []
+# 我的笔记评论缓存
+_cached_my_note_entries: list[dict] = []
+
+
+def _fetch_and_cache(keyword, mcp_url, cache_name="proactive"):
+ """通用: 获取笔记列表并缓存"""
+ global _cached_proactive_entries, _cached_my_note_entries
+ try:
+ client = MCPClient(mcp_url)
+ if keyword and keyword.strip():
+ entries = client.search_feeds_parsed(keyword.strip())
+ src = f"搜索「{keyword.strip()}」"
+ else:
+ entries = client.list_feeds_parsed()
+ src = "首页推荐"
+
+ if cache_name == "proactive":
+ _cached_proactive_entries = entries
+ else:
+ _cached_my_note_entries = entries
+
+ if not entries:
+ return gr.update(choices=[], value=None), f"⚠️ 从{src}未找到笔记"
+
+ choices = []
+ for i, e in enumerate(entries):
+ title_short = (e["title"] or "无标题")[:28]
+ label = f"[{i+1}] {title_short} | @{e['author'] or '未知'} | ❤ {e['likes']}"
+ choices.append(label)
+
+ return (
+ gr.update(choices=choices, value=choices[0]),
+ f"✅ 从{src}获取 {len(entries)} 条笔记",
+ )
+ except Exception as e:
+ if cache_name == "proactive":
+ _cached_proactive_entries = []
+ else:
+ _cached_my_note_entries = []
+ return gr.update(choices=[], value=None), f"❌ {e}"
+
+
+def _pick_from_cache(selected, cache_name="proactive"):
+ """通用: 从缓存中提取选中条目的 feed_id / xsec_token / title"""
+ cache = _cached_proactive_entries if cache_name == "proactive" else _cached_my_note_entries
+ if not selected or not cache:
+ return "", "", ""
+ try:
+ idx = int(selected.split("]")[0].replace("[", "")) - 1
+ e = cache[idx]
+ return e["feed_id"], e["xsec_token"], e.get("title", "")
+ except (ValueError, IndexError):
+ return "", "", ""
+
+
+# ---- 模块 A: 主动评论他人 ----
+
+def fetch_proactive_notes(keyword, mcp_url):
+ return _fetch_and_cache(keyword, mcp_url, "proactive")
+
+
+def on_proactive_note_selected(selected):
+ return _pick_from_cache(selected, "proactive")
+
+
+def load_note_for_comment(feed_id, xsec_token, mcp_url):
+ """加载目标笔记详情 (标题+正文+已有评论), 用于 AI 分析"""
+ if not feed_id or not xsec_token:
+ return "❌ 请先选择笔记", "", "", ""
+ try:
+ client = MCPClient(mcp_url)
+ result = client.get_feed_detail(feed_id, xsec_token, load_all_comments=True)
+ if "error" in result:
+ return f"❌ {result['error']}", "", "", ""
+ full_text = result.get("text", "")
+ # 尝试分离正文和评论
+ if "评论" in full_text:
+ parts = full_text.split("评论", 1)
+ content_part = parts[0].strip()
+ comments_part = "评论" + parts[1] if len(parts) > 1 else ""
+ else:
+ content_part = full_text[:500]
+ comments_part = ""
+ return "✅ 笔记内容已加载", content_part[:800], comments_part[:1500], full_text
+ except Exception as e:
+ return f"❌ {e}", "", "", ""
+
+
+def ai_generate_comment(api_key, base_url, model, persona,
+ post_title, post_content, existing_comments):
+ """AI 生成主动评论"""
+ if not api_key or not base_url:
+ return "⚠️ 请先配置 API Key", "❌ LLM 未配置"
+ if not model:
+ return "⚠️ 请先连接 LLM", "❌ 未选模型"
+ if not post_title and not post_content:
+ return "⚠️ 请先加载笔记内容", "❌ 无笔记内容"
+ try:
+ svc = LLMService(api_key, base_url, model)
+ comment = svc.generate_proactive_comment(
+ persona, post_title, post_content[:600], existing_comments[:800]
+ )
+ return comment, "✅ 评论已生成"
+ except Exception as e:
+ logger.error(f"AI 评论生成失败: {e}")
+ return f"生成失败: {e}", f"❌ {e}"
+
+
+def send_comment(feed_id, xsec_token, comment_content, mcp_url):
+ """发送评论到别人的笔记"""
+ if not all([feed_id, xsec_token, comment_content]):
+ return "❌ 缺少必要参数 (笔记ID / token / 评论内容)"
+ try:
+ client = MCPClient(mcp_url)
+ result = client.post_comment(feed_id, xsec_token, comment_content)
+ if "error" in result:
+ return f"❌ {result['error']}"
+ return "✅ 评论已发送!"
+ except Exception as e:
+ return f"❌ {e}"
+
+
+# ---- 模块 B: 回复我的笔记评论 ----
+
+def fetch_my_notes(keyword, mcp_url):
+ return _fetch_and_cache(keyword, mcp_url, "my_notes")
+
+
+def on_my_note_selected(selected):
+ return _pick_from_cache(selected, "my_notes")
+
+
+def fetch_my_note_comments(feed_id, xsec_token, mcp_url):
+ """获取我的笔记的评论列表"""
+ if not feed_id or not xsec_token:
+ return "❌ 请先选择笔记", ""
+ try:
+ client = MCPClient(mcp_url)
+ result = client.get_feed_detail(feed_id, xsec_token, load_all_comments=True)
+ if "error" in result:
+ return f"❌ {result['error']}", ""
+ return "✅ 评论加载完成", result.get("text", "暂无评论")
+ except Exception as e:
+ return f"❌ {e}", ""
+
+
+def ai_reply_comment(api_key, base_url, model, persona, post_title, comment_text):
+ """AI 生成评论回复"""
+ if not api_key or not base_url:
+ return "⚠️ 请先在全局设置中填写 API Key 和 Base URL", "❌ LLM 未配置"
+ if not model:
+ return "⚠️ 请先连接 LLM 并选择模型", "❌ 未选择模型"
+ if not comment_text:
+ return "请输入需要回复的评论内容", "⚠️ 请输入评论"
+ try:
+ svc = LLMService(api_key, base_url, model)
+ reply = svc.generate_reply(persona, post_title, comment_text)
+ return reply, "✅ 回复已生成"
+ except Exception as e:
+ logger.error(f"AI 回复生成失败: {e}")
+ return f"生成失败: {e}", f"❌ {e}"
+
+
+def send_reply(feed_id, xsec_token, reply_content, mcp_url):
+ """发送评论回复"""
+ if not all([feed_id, xsec_token, reply_content]):
+ return "❌ 缺少必要参数"
+ try:
+ client = MCPClient(mcp_url)
+ result = client.post_comment(feed_id, xsec_token, reply_content)
+ if "error" in result:
+ return f"❌ 回复失败: {result['error']}"
+ return "✅ 回复已发送"
+ except Exception as e:
+ return f"❌ 发送失败: {e}"
+
+
+# ==================================================
+# Tab 4: 数据看板
+# ==================================================
+
+
+# 全局缓存: 最近获取的用户列表
+_cached_user_entries: list[dict] = []
+
+
+def fetch_user_list_from_feeds(mcp_url):
+ """从推荐列表中提取用户列表, 供数据看板使用"""
+ global _cached_user_entries
+ try:
+ client = MCPClient(mcp_url)
+ entries = client.list_feeds_parsed()
+
+ # 去重: 按 user_id
+ seen = set()
+ users = []
+ for e in entries:
+ uid = e.get("user_id", "")
+ if uid and uid not in seen:
+ seen.add(uid)
+ users.append({
+ "user_id": uid,
+ "xsec_token": e.get("xsec_token", ""),
+ "nickname": e.get("author", "未知"),
+ })
+
+ _cached_user_entries = users
+
+ if not users:
+ return gr.update(choices=[], value=None), "⚠️ 未找到用户信息"
+
+ choices = [
+ f"@{u['nickname']} ({u['user_id'][:8]}...)"
+ for u in users
+ ]
+ return (
+ gr.update(choices=choices, value=choices[0]),
+ f"✅ 发现 {len(users)} 位用户,请在下拉框中选择",
+ )
+ except Exception as e:
+ _cached_user_entries = []
+ return gr.update(choices=[], value=None), f"❌ {e}"
+
+
+def on_user_selected(selected_user):
+ """用户下拉框选择回调, 自动填充 user_id 和 xsec_token"""
+ global _cached_user_entries
+ if not selected_user or not _cached_user_entries:
+ return gr.update(), gr.update()
+
+ # 匹配 "(user_id_prefix...)"
+ for u in _cached_user_entries:
+ if u["user_id"][:8] in selected_user:
+ return u["user_id"], u["xsec_token"]
+
+ return gr.update(), gr.update()
+
+
+def fetch_user_data(user_id, xsec_token, mcp_url):
+ """获取用户主页数据"""
+ if not user_id or not xsec_token:
+ return "❌ 请填写用户 ID 和 xsec_token", ""
+ try:
+ client = MCPClient(mcp_url)
+ result = client.get_user_profile(user_id, xsec_token)
+ if "error" in result:
+ return f"❌ 获取失败: {result['error']}", ""
+ return "✅ 数据加载完成", result.get("text", "无数据")
+ except Exception as e:
+ return f"❌ 获取数据失败: {e}", ""
+
+
+def fetch_homepage_feeds(mcp_url):
+ """获取首页推荐"""
+ try:
+ client = MCPClient(mcp_url)
+ result = client.list_feeds()
+ if "error" in result:
+ return f"❌ {result['error']}", ""
+ return "✅ 推荐列表已加载", result.get("text", "无数据")
+ except Exception as e:
+ return f"❌ {e}", ""
+
+
+# ==================================================
+# UI 构建
+# ==================================================
+
+config = cfg.all
+
+with gr.Blocks(
+ title="小红书 AI 爆文工坊 V2.0",
+ theme=gr.themes.Soft(),
+ css="""
+ .status-ok { color: #16a34a; font-weight: bold; }
+ .status-err { color: #dc2626; font-weight: bold; }
+ footer { display: none !important; }
+ """,
+) as app:
+ gr.Markdown(
+ "# 🍒 小红书 AI 爆文生产工坊 V2.0\n"
+ "> 灵感 → 文案 → 绘图 → 发布 → 运营,一站式全闭环"
+ )
+
+ # 全局状态
+ state_images = gr.State([])
+ state_search_result = gr.State("")
+
+ # ============ 全局设置栏 ============
+ with gr.Accordion("⚙️ 全局设置 (自动保存)", open=False):
+ with gr.Row():
+ api_key = gr.Textbox(
+ label="LLM API Key", value=config["api_key"],
+ type="password", scale=2,
+ )
+ base_url = gr.Textbox(
+ label="LLM Base URL", value=config["base_url"], scale=2,
+ )
+ mcp_url = gr.Textbox(
+ label="MCP Server URL", value=config["mcp_url"], scale=2,
+ )
+ with gr.Row():
+ sd_url = gr.Textbox(
+ label="SD WebUI URL", value=config["sd_url"], scale=2,
+ )
+ persona = gr.Textbox(
+ label="博主人设(评论回复用)",
+ value=config["persona"], scale=3,
+ )
+ with gr.Row():
+ btn_connect_llm = gr.Button("🔗 连接 LLM", size="sm")
+ btn_connect_sd = gr.Button("🎨 连接 SD", size="sm")
+ btn_check_mcp = gr.Button("📡 检查 MCP", size="sm")
+ with gr.Row():
+ llm_model = gr.Dropdown(
+ label="LLM 模型", value=config["model"],
+ allow_custom_value=True, interactive=True, scale=2,
+ )
+ sd_model = gr.Dropdown(
+ label="SD 模型", allow_custom_value=True,
+ interactive=True, scale=2,
+ )
+ status_bar = gr.Markdown("🔄 等待连接...")
+
+ # ============ Tab 页面 ============
+ with gr.Tabs():
+ # -------- Tab 1: 内容创作 --------
+ with gr.Tab("✨ 内容创作"):
+ with gr.Row():
+ # 左栏:输入
+ with gr.Column(scale=1):
+ gr.Markdown("### 💡 构思")
+ topic = gr.Textbox(label="笔记主题", placeholder="例如:优衣库早春穿搭")
+ style = gr.Dropdown(
+ ["好物种草", "干货教程", "情绪共鸣", "生活Vlog", "测评避雷", "知识科普"],
+ label="风格", value="好物种草",
+ )
+ btn_gen_copy = gr.Button("✨ 第一步:生成文案", variant="primary")
+
+ gr.Markdown("---")
+ gr.Markdown("### 🎨 绘图参数")
+ with gr.Accordion("高级设置", open=False):
+ neg_prompt = gr.Textbox(
+ label="反向提示词", value=DEFAULT_NEGATIVE, lines=2,
+ )
+ steps = gr.Slider(15, 50, value=25, step=1, label="步数")
+ cfg_scale = gr.Slider(1, 15, value=7, step=0.5, label="CFG Scale")
+ btn_gen_img = gr.Button("🎨 第二步:生成图片", variant="primary")
+
+ # 中栏:文案编辑
+ with gr.Column(scale=1):
+ gr.Markdown("### 📝 文案编辑")
+ res_title = gr.Textbox(label="标题 (≤20字)", interactive=True)
+ res_content = gr.TextArea(
+ label="正文 (可手动修改)", lines=12, interactive=True,
+ )
+ res_prompt = gr.TextArea(
+ label="绘图提示词", lines=3, interactive=True,
+ )
+ res_tags = gr.Textbox(
+ label="话题标签 (逗号分隔)", interactive=True,
+ placeholder="穿搭, 春季, 好物种草",
+ )
+
+ # 右栏:预览 & 发布
+ with gr.Column(scale=1):
+ gr.Markdown("### 🖼️ 视觉预览")
+ gallery = gr.Gallery(label="AI 生成图片", columns=2, height=300)
+ local_images = gr.File(
+ label="📁 上传本地图片(可混排)",
+ file_count="multiple",
+ file_types=["image"],
+ )
+
+ gr.Markdown("### 🚀 发布")
+ schedule_time = gr.Textbox(
+ label="定时发布 (可选, ISO8601格式)",
+ placeholder="如 2026-02-08T18:00:00+08:00,留空=立即发布",
+ )
+ with gr.Row():
+ btn_export = gr.Button("📂 导出本地", variant="secondary")
+ btn_publish = gr.Button("🚀 发布到小红书", variant="primary")
+ publish_msg = gr.Markdown("")
+
+ # -------- Tab 2: 热点探测 --------
+ with gr.Tab("🔥 热点探测"):
+ gr.Markdown("### 搜索热门内容 → AI 分析趋势 → 一键借鉴创作")
+ with gr.Row():
+ with gr.Column(scale=1):
+ hot_keyword = gr.Textbox(
+ label="搜索关键词", placeholder="如:春季穿搭",
+ )
+ hot_sort = gr.Dropdown(
+ ["综合", "最新", "最多点赞", "最多评论", "最多收藏"],
+ label="排序", value="综合",
+ )
+ btn_search = gr.Button("🔍 搜索", variant="primary")
+ search_status = gr.Markdown("")
+
+ with gr.Column(scale=2):
+ search_output = gr.TextArea(
+ label="搜索结果", lines=12, interactive=False,
+ )
+
+ with gr.Row():
+ btn_analyze = gr.Button("🧠 AI 分析热点趋势", variant="primary")
+ analysis_status = gr.Markdown("")
+ analysis_output = gr.Markdown(label="分析报告")
+ topic_from_hot = gr.Textbox(
+ label="选择/输入创作选题", placeholder="基于分析选一个方向",
+ )
+
+ with gr.Row():
+ hot_style = gr.Dropdown(
+ ["好物种草", "干货教程", "情绪共鸣", "生活Vlog", "测评避雷"],
+ label="风格", value="好物种草",
+ )
+ btn_gen_from_hot = gr.Button("✨ 基于热点生成文案", variant="primary")
+
+ with gr.Row():
+ hot_title = gr.Textbox(label="生成的标题", interactive=True)
+ hot_content = gr.TextArea(label="生成的正文", lines=8, interactive=True)
+ with gr.Row():
+ hot_prompt = gr.TextArea(label="绘图提示词", lines=3, interactive=True)
+ hot_tags = gr.Textbox(label="标签", interactive=True)
+ hot_gen_status = gr.Markdown("")
+ btn_sync_to_create = gr.Button(
+ "📋 同步到「内容创作」Tab → 绘图 & 发布",
+ variant="primary",
+ )
+
+ # -------- Tab 3: 评论管家 --------
+ with gr.Tab("💬 评论管家"):
+ gr.Markdown("### 智能评论管理:主动评论引流 & 自动回复粉丝")
+
+ with gr.Tabs():
+ # ======== 子 Tab A: 主动评论他人 ========
+ with gr.Tab("✍️ 主动评论引流"):
+ gr.Markdown(
+ "> **流程**:搜索/浏览笔记 → 选择目标 → 加载内容 → "
+ "AI 分析笔记+已有评论自动生成高质量评论 → 一键发送"
+ )
+
+ # 笔记选择器
+ with gr.Row():
+ pro_keyword = gr.Textbox(
+ label="🔍 搜索关键词 (留空则获取推荐)",
+ placeholder="穿搭、美食、旅行…",
+ )
+ btn_pro_fetch = gr.Button("🔍 获取笔记", variant="primary")
+ with gr.Row():
+ pro_selector = gr.Dropdown(
+ label="📋 选择目标笔记",
+ choices=[], interactive=True,
+ )
+ pro_fetch_status = gr.Markdown("")
+
+ # 隐藏字段
+ with gr.Row():
+ pro_feed_id = gr.Textbox(label="笔记 ID", interactive=False)
+ pro_xsec_token = gr.Textbox(label="xsec_token", interactive=False)
+ pro_title = gr.Textbox(label="标题", interactive=False)
+
+ # 加载内容 & AI 分析
+ btn_pro_load = gr.Button("📖 加载笔记内容", variant="secondary")
+ pro_load_status = gr.Markdown("")
+
+ with gr.Row():
+ with gr.Column(scale=1):
+ pro_content = gr.TextArea(
+ label="📄 笔记正文摘要", lines=8, interactive=False,
+ )
+ with gr.Column(scale=1):
+ pro_comments = gr.TextArea(
+ label="💬 已有评论", lines=8, interactive=False,
+ )
+ # 隐藏: 完整文本
+ pro_full_text = gr.Textbox(visible=False)
+
+ gr.Markdown("---")
+ with gr.Row():
+ with gr.Column(scale=1):
+ btn_pro_ai = gr.Button(
+ "🤖 AI 智能生成评论", variant="primary", size="lg",
+ )
+ pro_ai_status = gr.Markdown("")
+ with gr.Column(scale=2):
+ pro_comment_text = gr.TextArea(
+ label="✏️ 评论内容 (可手动修改)", lines=3,
+ interactive=True,
+ placeholder="点击左侧按钮自动生成,也可手动编写",
+ )
+ with gr.Row():
+ btn_pro_send = gr.Button("📩 发送评论", variant="primary")
+ pro_send_status = gr.Markdown("")
+
+ # ======== 子 Tab B: 回复我的评论 ========
+ with gr.Tab("💌 回复粉丝评论"):
+ gr.Markdown(
+ "> **流程**:选择我的笔记 → 加载评论 → "
+ "粘贴要回复的评论 → AI 生成回复 → 一键发送"
+ )
+
+ # 笔记选择器
+ with gr.Row():
+ my_keyword = gr.Textbox(
+ label="🔍 搜索我的笔记关键词 (留空获取推荐)",
+ placeholder="我发布过的笔记关键词…",
+ )
+ btn_my_fetch = gr.Button("🔍 获取笔记", variant="primary")
+ with gr.Row():
+ my_selector = gr.Dropdown(
+ label="📋 选择我的笔记",
+ choices=[], interactive=True,
+ )
+ my_fetch_status = gr.Markdown("")
+
+ with gr.Row():
+ my_feed_id = gr.Textbox(label="笔记 ID", interactive=False)
+ my_xsec_token = gr.Textbox(label="xsec_token", interactive=False)
+ my_title = gr.Textbox(label="笔记标题", interactive=False)
+
+ btn_my_load_comments = gr.Button("📥 加载评论", variant="primary")
+ my_comment_status = gr.Markdown("")
+
+ my_comments_display = gr.TextArea(
+ label="📋 粉丝评论列表", lines=12, interactive=False,
+ )
+
+ gr.Markdown("---")
+ gr.Markdown("#### 📝 回复评论")
+ with gr.Row():
+ with gr.Column(scale=1):
+ my_target_comment = gr.TextArea(
+ label="要回复的评论内容", lines=3,
+ placeholder="从上方评论列表中复制粘贴要回复的评论",
+ )
+ btn_my_ai_reply = gr.Button(
+ "🤖 AI 生成回复", variant="secondary",
+ )
+ my_reply_gen_status = gr.Markdown("")
+ with gr.Column(scale=1):
+ my_reply_content = gr.TextArea(
+ label="回复内容 (可修改)", lines=3,
+ interactive=True,
+ )
+ btn_my_send_reply = gr.Button(
+ "📩 发送回复", variant="primary",
+ )
+ my_reply_status = gr.Markdown("")
+
+ # -------- Tab 4: 账号登录 --------
+ with gr.Tab("🔐 账号登录"):
+ gr.Markdown(
+ "### 小红书账号登录\n"
+ "> 点击获取二维码 → 用小红书 App 扫码 → 确认登录 → 检查状态"
+ )
+ with gr.Row():
+ with gr.Column(scale=1):
+ gr.Markdown(
+ "**操作步骤:**\n"
+ "1. 确保 MCP 服务已启动\n"
+ "2. 点击「获取登录二维码」\n"
+ "3. 用小红书 App 扫码并确认\n"
+ "4. 点击「检查登录状态」验证\n\n"
+ "⚠️ 登录后不要在其他网页端登录同一账号,否则会被踢出"
+ )
+ btn_get_qrcode = gr.Button(
+ "📱 获取登录二维码", variant="primary", size="lg",
+ )
+ btn_check_login = gr.Button(
+ "🔍 检查登录状态", variant="secondary", size="lg",
+ )
+ login_status = gr.Markdown("🔄 等待操作...")
+
+ with gr.Column(scale=1):
+ qr_image = gr.Image(
+ label="扫码登录", height=350, width=350,
+ )
+
+ # -------- Tab 5: 数据看板 --------
+ with gr.Tab("📊 数据看板"):
+ gr.Markdown("### 账号数据概览")
+
+ # ---- 用户选择器 ----
+ gr.Markdown("#### 🔍 快速选择用户 (从推荐列表提取)")
+ with gr.Row():
+ btn_fetch_users = gr.Button(
+ "👥 从推荐获取用户", variant="primary",
+ )
+ user_selector = gr.Dropdown(
+ label="选择用户 (自动填充下方 ID)",
+ choices=[], interactive=True,
+ )
+ user_fetch_status = gr.Markdown("")
+
+ gr.Markdown("---")
+ with gr.Row():
+ with gr.Column(scale=1):
+ data_user_id = gr.Textbox(label="用户 ID")
+ data_xsec_token = gr.Textbox(label="xsec_token")
+ btn_load_profile = gr.Button("📊 加载用户数据", variant="primary")
+ data_status = gr.Markdown("")
+
+ gr.Markdown("---")
+ btn_load_feeds = gr.Button("🏠 查看首页推荐", variant="secondary")
+ feeds_status = gr.Markdown("")
+
+ with gr.Column(scale=2):
+ profile_display = gr.TextArea(
+ label="用户信息 & 笔记数据", lines=15,
+ interactive=False,
+ )
+ feeds_display = gr.TextArea(
+ label="首页推荐", lines=10, interactive=False,
+ )
+
+ # ==================================================
+ # 事件绑定
+ # ==================================================
+
+ # ---- 全局设置 ----
+ btn_connect_llm.click(
+ fn=connect_llm, inputs=[api_key, base_url],
+ outputs=[llm_model, status_bar],
+ )
+ btn_connect_sd.click(
+ fn=connect_sd, inputs=[sd_url],
+ outputs=[sd_model, status_bar],
+ )
+ btn_check_mcp.click(
+ fn=check_mcp_status, inputs=[mcp_url],
+ outputs=[status_bar],
+ )
+
+ # ---- Tab 1: 内容创作 ----
+ btn_gen_copy.click(
+ fn=generate_copy,
+ inputs=[api_key, base_url, llm_model, topic, style],
+ outputs=[res_title, res_content, res_prompt, res_tags, status_bar],
+ )
+
+ btn_gen_img.click(
+ fn=generate_images,
+ 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=[publish_msg],
+ )
+
+ btn_publish.click(
+ fn=publish_to_xhs,
+ inputs=[res_title, res_content, res_tags, state_images,
+ local_images, mcp_url, schedule_time],
+ outputs=[publish_msg],
+ )
+
+ # ---- Tab 2: 热点探测 ----
+ btn_search.click(
+ fn=search_hotspots,
+ inputs=[hot_keyword, hot_sort, mcp_url],
+ outputs=[search_status, search_output],
+ )
+ # 搜索结果同步到 state
+ search_output.change(
+ fn=lambda x: x, inputs=[search_output], outputs=[state_search_result],
+ )
+
+ btn_analyze.click(
+ fn=analyze_and_suggest,
+ inputs=[api_key, base_url, llm_model, hot_keyword, search_output],
+ outputs=[analysis_status, analysis_output, topic_from_hot],
+ )
+
+ btn_gen_from_hot.click(
+ fn=generate_from_hotspot,
+ inputs=[api_key, base_url, llm_model, topic_from_hot, hot_style, search_output],
+ outputs=[hot_title, hot_content, hot_prompt, hot_tags, hot_gen_status],
+ )
+
+ # 同步热点文案到内容创作 Tab
+ btn_sync_to_create.click(
+ fn=lambda t, c, p, tg: (t, c, p, tg, "✅ 已同步到「内容创作」,可切换 Tab 继续绘图和发布"),
+ inputs=[hot_title, hot_content, hot_prompt, hot_tags],
+ outputs=[res_title, res_content, res_prompt, res_tags, status_bar],
+ )
+
+ # ---- Tab 3: 评论管家 ----
+
+ # == 子 Tab A: 主动评论引流 ==
+ btn_pro_fetch.click(
+ fn=fetch_proactive_notes,
+ inputs=[pro_keyword, mcp_url],
+ outputs=[pro_selector, pro_fetch_status],
+ )
+ pro_selector.change(
+ fn=on_proactive_note_selected,
+ inputs=[pro_selector],
+ outputs=[pro_feed_id, pro_xsec_token, pro_title],
+ )
+ btn_pro_load.click(
+ fn=load_note_for_comment,
+ inputs=[pro_feed_id, pro_xsec_token, mcp_url],
+ outputs=[pro_load_status, pro_content, pro_comments, pro_full_text],
+ )
+ btn_pro_ai.click(
+ fn=ai_generate_comment,
+ inputs=[api_key, base_url, llm_model, persona,
+ pro_title, pro_content, pro_comments],
+ outputs=[pro_comment_text, pro_ai_status],
+ )
+ btn_pro_send.click(
+ fn=send_comment,
+ inputs=[pro_feed_id, pro_xsec_token, pro_comment_text, mcp_url],
+ outputs=[pro_send_status],
+ )
+
+ # == 子 Tab B: 回复粉丝评论 ==
+ btn_my_fetch.click(
+ fn=fetch_my_notes,
+ inputs=[my_keyword, mcp_url],
+ outputs=[my_selector, my_fetch_status],
+ )
+ my_selector.change(
+ fn=on_my_note_selected,
+ inputs=[my_selector],
+ outputs=[my_feed_id, my_xsec_token, my_title],
+ )
+ btn_my_load_comments.click(
+ fn=fetch_my_note_comments,
+ inputs=[my_feed_id, my_xsec_token, mcp_url],
+ outputs=[my_comment_status, my_comments_display],
+ )
+ btn_my_ai_reply.click(
+ fn=ai_reply_comment,
+ inputs=[api_key, base_url, llm_model, persona,
+ my_title, my_target_comment],
+ outputs=[my_reply_content, my_reply_gen_status],
+ )
+ btn_my_send_reply.click(
+ fn=send_reply,
+ inputs=[my_feed_id, my_xsec_token, my_reply_content, mcp_url],
+ outputs=[my_reply_status],
+ )
+
+ # ---- Tab 4: 账号登录 ----
+ btn_get_qrcode.click(
+ fn=get_login_qrcode,
+ inputs=[mcp_url],
+ outputs=[qr_image, login_status],
+ )
+ btn_check_login.click(
+ fn=check_login,
+ inputs=[mcp_url],
+ outputs=[login_status],
+ )
+
+ # ---- Tab 5: 数据看板 ----
+ # 从推荐获取用户列表
+ btn_fetch_users.click(
+ fn=fetch_user_list_from_feeds,
+ inputs=[mcp_url],
+ outputs=[user_selector, user_fetch_status],
+ )
+ # 选择用户 -> 自动填充 user_id / xsec_token
+ user_selector.change(
+ fn=on_user_selected,
+ inputs=[user_selector],
+ outputs=[data_user_id, data_xsec_token],
+ )
+
+ btn_load_profile.click(
+ fn=fetch_user_data,
+ inputs=[data_user_id, data_xsec_token, mcp_url],
+ outputs=[data_status, profile_display],
+ )
+
+ btn_load_feeds.click(
+ fn=fetch_homepage_feeds,
+ inputs=[mcp_url],
+ outputs=[feeds_status, feeds_display],
+ )
+
+ # ---- 启动时自动刷新 SD ----
+ app.load(fn=connect_sd, inputs=[sd_url], outputs=[sd_model, status_bar])
+
+
+# ==================================================
+if __name__ == "__main__":
+ logger.info("🍒 小红书 AI 爆文工坊 V2.0 启动中...")
+ app.launch(inbrowser=True, share=False)
diff --git a/main_v1_backup.py b/main_v1_backup.py
new file mode 100644
index 0000000..abbcd12
--- /dev/null
+++ b/main_v1_backup.py
@@ -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)
\ No newline at end of file
diff --git a/mcp.md b/mcp.md
new file mode 100644
index 0000000..b5e0e75
--- /dev/null
+++ b/mcp.md
@@ -0,0 +1,857 @@
+# xiaohongshu-mcp
+
+
+[](#contributors-)
+
+
+[](./DONATIONS.md)
+[](./DONATIONS.md)
+[](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
+
+[](https://www.star-history.com/#xpzouying/xiaohongshu-mcp&Timeline)
+
+## 赞赏支持
+
+本项目所有的赞赏都会用于慈善捐赠。所有的慈善捐赠记录,请参考 [DONATIONS.md](./DONATIONS.md)。
+
+**捐赠时,请备注 MCP 以及名字。**
+如需更正/撤回署名,请开 Issue 或通过邮箱联系。
+
+**支付宝(不展示二维码):**
+
+通过支付宝向 **xpzouying@gmail.com** 赞赏。
+
+**微信:**
+
+
+
+## 项目简介
+
+**主要功能**
+
+> 💡 **提示:** 点击下方功能标题可展开查看视频演示
+
+1. 登录和检查登录状态
+
+第一步必须,小红书需要进行登录。可以检查当前登录状态。
+
+**登录演示:**
+
+https://github.com/user-attachments/assets/8b05eb42-d437-41b7-9235-e2143f19e8b7
+
+**检查登录状态演示:**
+
+https://github.com/user-attachments/assets/bd9a9a4a-58cb-4421-b8f3-015f703ce1f9
+
+2. 发布图文内容
+
+支持发布图文内容到小红书,包括标题、内容描述和图片。
+
+**图片支持方式:**
+
+支持两种图片输入方式:
+
+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
+
+3. 发布视频内容
+
+支持发布视频内容到小红书,包括标题、内容描述和本地视频文件。
+
+**视频支持方式:**
+
+仅支持本地视频文件绝对路径:
+
+```
+"/Users/username/Videos/video.mp4"
+```
+
+**功能特点:**
+
+- ✅ 支持本地视频文件上传
+- ✅ 自动处理视频格式转换
+- ✅ 支持标题、内容描述和标签
+- ✅ 等待视频处理完成后自动发布
+
+**注意事项:**
+
+- 仅支持本地视频文件,不支持 HTTP 链接
+- 视频处理时间较长,请耐心等待
+- 建议视频文件大小不超过 1GB
+
+4. 搜索内容
+
+根据关键词搜索小红书内容。
+
+**搜索帖子演示:**
+
+https://github.com/user-attachments/assets/03c5077d-6160-4b18-b629-2e40933a1fd3
+
+5. 获取推荐列表
+
+获取小红书首页推荐内容列表。
+
+**获取推荐列表演示:**
+
+https://github.com/user-attachments/assets/110fc15d-46f2-4cca-bdad-9de5b5b8cc28
+
+6. 获取帖子详情(包括互动数据和评论)
+
+获取小红书帖子的完整详情,包括:
+
+- 帖子内容(标题、描述、图片等)
+- 用户信息
+- 互动数据(点赞、收藏、分享、评论数)
+- 评论列表及子评论
+
+**⚠️ 重要提示:**
+
+- 需要提供帖子 ID 和 xsec_token(两个参数缺一不可)
+- 这两个参数可以从 Feed 列表或搜索结果中获取
+- 必须先登录才能使用此功能
+
+**获取帖子详情演示:**
+
+https://github.com/user-attachments/assets/76a26130-a216-4371-a6b3-937b8fda092a
+
+7. 发表评论到帖子
+
+支持自动发表评论到小红书帖子。
+
+**功能说明:**
+
+- 自动定位评论输入框
+- 输入评论内容并发布
+- 支持 HTTP API 和 MCP 工具调用
+
+**⚠️ 重要提示:**
+
+- 需要先登录才能使用此功能
+- 需要提供帖子 ID、xsec_token 和评论内容
+- 这些参数可以从 Feed 列表或搜索结果中获取
+
+**发表评论演示:**
+
+https://github.com/user-attachments/assets/cc385b6c-422c-489b-a5fc-63e92c695b80
+
+8. 获取用户个人主页
+
+获取小红书用户的个人主页信息,包括用户基本信息和笔记内容。
+
+**功能说明:**
+
+- 获取用户基本信息(昵称、简介、头像等)
+- 获取关注数、粉丝数、获赞量统计
+- 获取用户发布的笔记内容列表
+- 支持 HTTP API 和 MCP 工具调用
+
+**⚠️ 重要提示:**
+
+- 需要先登录才能使用此功能
+- 需要提供用户 ID 和 xsec_token
+- 这些参数可以从 Feed 列表或搜索结果中获取
+
+**返回信息包括:**
+
+- 用户基本信息:昵称、简介、头像、认证状态
+- 统计数据:关注数、粉丝数、获赞量、笔记数
+- 笔记列表:用户发布的所有公开笔记
+
+
+
+
+
+一周左右的成果
+
+
+
+## 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),请确保网络连接正常。后续运行无需重复下载。
+
+**方式二:源码编译**
+
+
源码编译安装详情
+
+依赖 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
+```
+
+Docker 部署详情
+
+使用 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)
+
+Claude Code CLI
+
+官方命令行工具,已在上面快速开始部分展示:
+
+```bash
+# 添加 HTTP MCP 服务器
+claude mcp add --transport http xiaohongshu-mcp http://localhost:18060/mcp
+
+# 检查 MCP 是否添加成功(确保 MCP 已经启动的前提下,运行下面命令)
+claude mcp list
+```
+
+Cursor
+
+#### 配置文件的方式
+
+创建或编辑 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 接入:
+
+
+
+调用 MCP 工具:(以检查登录状态为例)
+
+
+
+VSCode
+
+#### 方法一:使用命令面板配置
+
+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": []
+}
+```
+
+**查看配置**:
+
+
+
+1. 确认运行状态。
+2. 查看 `tools` 是否正确检测。
+
+**Demo**
+
+以搜索帖子内容为例:
+
+
+
+Google Gemini CLI
+
+在 `~/.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)
+
+MCP Inspector
+
+调试工具,用于测试 MCP 连接:
+
+```bash
+# 启动 MCP Inspector
+npx @modelcontextprotocol/inspector
+
+# 在浏览器中连接到:http://localhost:18060/mcp
+```
+
+使用步骤:
+
+- 使用 MCP Inspector 测试连接
+- 测试 Ping Server 功能验证连接
+- 检查 List Tools 是否返回 6 个工具
+
+Cline
+
+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
+```
+
+```
+搜索小红书上关于"美食"的内容
+```
+
+其他支持 HTTP MCP 的客户端
+
+任何支持 HTTP MCP 协议的客户端都可以连接到:`http://localhost:18060/mcp`
+
+基本配置模板:
+
+```json
+{
+ "name": "xiaohongshu-mcp",
+ "url": "http://localhost:18060/mcp",
+ "type": "http"
+}
+```
+
+
+
+### 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 群】:扫码进入 |
+| ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| |
|
+
+
+
+
+## 🙏 致谢贡献者 ✨
+
+感谢以下所有为本项目做出贡献的朋友!(排名不分先后)
+
+
+
+
+
zy 💻 🤔 📖 🎨 🚧 🚇 👀 |
+ clearwater 💻 |
+ Zhongpeng 💻 |
+ Duong Tran 💻 |
+ Angiin 💻 |
+ Henan Mu 💻 |
+ Journey 💻 |
+
Eve Yu 💻 |
+ CooperGuo 💻 |
+ Banghao Chi 💻 |
+ varz1 💻 |
+ Melo Y Guan 💻 |
+ lmxdawn 💻 |
+ haikow 💻 |
+
Carlo 💻 |
+ hrz 💻 |
+ Ctrlz 💻 |
+ flippancy 💻 |
+ Yuhang Lu 💻 |
+ Bryan Thompson 💻 |
+ tan jun 💻 |
+