From d782bb6781ba8785b5e9dbd250ada16454aae9d7 Mon Sep 17 00:00:00 2001
From: zhoujie <929834232@qq.com>
Date: Sun, 8 Feb 2026 22:40:16 +0800
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(auto):=20=E6=96=B0=E5=A2=9E?=
=?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=82=B9=E8=B5=9E=E5=92=8C=E8=87=AA=E5=8A=A8?=
=?UTF-8?q?=E5=9B=9E=E5=A4=8D=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=AE=8C=E5=96=84?=
=?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增自动点赞功能【一键点赞】:支持关键词搜索笔记并随机批量点赞,提升账号活跃度
- 新增自动回复功能【一键回复】:自动扫描用户笔记的粉丝评论,使用AI生成并发送回复
- 扩展自动化调度器【定时调度】:支持点赞和回复任务的随机定时执行,模拟真人操作间隔
- 新增项目文档【文档】:添加README、CHANGELOG、CONTRIBUTING、LICENSE等核心文档文件
- 优化.gitignore文件【配置】:完善Python项目、IDE、敏感文件、日志等忽略规则
- 新增配置文件模板【配置】:提供config.example.json作为配置参考
- 优化MCP客户端【工具】:新增评论解析方法,支持从笔记详情中提取结构化评论数据
---
.gitignore | 48 +++++-
CHANGELOG.md | 51 +++++++
CONTRIBUTING.md | 118 +++++++++++++++
LICENSE | 21 +++
README.md | 263 +++++++++++++++++++++++++++++++++
config.example.json | 20 +++
main.py | 352 ++++++++++++++++++++++++++++++++++++++++++--
mcp_client.py | 55 +++++++
8 files changed, 914 insertions(+), 14 deletions(-)
create mode 100644 CHANGELOG.md
create mode 100644 CONTRIBUTING.md
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 config.example.json
diff --git a/.gitignore b/.gitignore
index 4287568..bf61413 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,45 @@
-xhs_workspace
-__pycache__
-*.log
\ No newline at end of file
+# ========== 工作空间 ==========
+xhs_workspace/
+
+# ========== Python ==========
+__pycache__/
+*.py[cod]
+*$py.class
+*.egg-info/
+dist/
+build/
+.eggs/
+
+# ========== 虚拟环境 ==========
+.venv/
+venv/
+env/
+
+# ========== 敏感配置 ==========
+config.json
+cookies.json
+*.cookie
+
+# ========== 日志 ==========
+*.log
+logs/
+
+# ========== IDE ==========
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# ========== 系统文件 ==========
+.DS_Store
+Thumbs.db
+desktop.ini
+
+# ========== 备份文件 ==========
+*_backup.py
+config copy.json
+
+# ========== 临时文件 ==========
+*.tmp
+*.bak
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ca83282
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,51 @@
+# 更新日志
+
+本项目遵循 [Semantic Versioning](https://semver.org/) 语义化版本规范。
+
+## [2.0.0] - 2026-02-08
+
+### 🚀 新功能
+
+- **自动运营模块**
+ - 一键智能评论:自动搜索高赞笔记 → AI 分析内容 → 生成评论 → 发送
+ - 一键自动点赞:批量搜索笔记并随机点赞,提升账号活跃度
+ - 一键自动回复:扫描我的笔记评论 → AI 生成回复 → 逐条发送
+ - 一键智能发布:自动生成主题 + 文案 + SD 绘图 + 发布到小红书
+ - 随机定时调度:评论/点赞/回复/发布四项可任意组合,随机间隔模拟真人
+ - 实时运行日志面板
+
+- **评论管家**
+ - 主动评论引流:浏览笔记 → AI 智能生成评论 → 一键发送
+ - 回复粉丝评论:加载我的笔记评论 → AI 回复 → 发送
+
+- **热点探测**
+ - 关键词搜索小红书热门笔记
+ - AI 趋势分析(标题套路、内容结构、推荐选题)
+ - 基于热点参考生成原创文案
+
+- **数据看板**
+ - 账号核心指标可视化(粉丝、获赞、收藏)
+ - 笔记点赞排行图表
+
+- **多 LLM 提供商**
+ - 支持添加/切换/删除多个 LLM 提供商
+ - 兼容所有 OpenAI 接口(DeepSeek、GPT、通义千问等)
+
+- **账号管理**
+ - 小红书扫码登录
+ - 自动获取 xsec_token
+
+### 🎨 优化
+
+- SD 参数针对 JuggernautXL/SDXL 优化(CFG 5.0、DPM++ 2M SDE Karras、832×1216)
+- 负面提示词增强(SDXL 专用)
+- LLM 绘图 Prompt 模板优化(photorealistic、cinematic 风格引导)
+
+## [1.0.0] - 2026-01-xx
+
+### 🚀 初始版本
+
+- 基础内容创作流程:主题输入 → LLM 文案 → SD 绘图 → 一键发布
+- 支持图文发布到小红书
+- 本地文案导出
+- Gradio Web UI
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2d609b1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,118 @@
+# 贡献指南
+
+感谢你对本项目的关注!我们欢迎任何形式的贡献,包括但不限于 Bug 报告、功能建议、代码提交和文档改进。
+
+## 开发环境搭建
+
+```bash
+# 1. Fork 并克隆项目
+git clone https://github.com/your-username/xhs-autobot.git
+cd xhs-autobot
+
+# 2. 创建虚拟环境
+python -m venv .venv
+# Windows
+.venv\Scripts\activate
+# macOS/Linux
+source .venv/bin/activate
+
+# 3. 安装依赖
+pip install -r requirements.txt
+
+# 4. 复制配置文件
+cp config.example.json config.json
+# 编辑 config.json 填写你的 API Key
+
+# 5. 启动开发
+python main.py
+```
+
+## 提交规范
+
+本项目使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
+
+```
+():
+
+[可选正文]
+
+[可选脚注]
+```
+
+### Type 类型
+
+| 类型 | 说明 |
+|------|------|
+| `feat` | 新功能 |
+| `fix` | Bug 修复 |
+| `docs` | 文档更新 |
+| `style` | 代码格式调整(不影响逻辑) |
+| `refactor` | 重构(非新功能、非修复) |
+| `perf` | 性能优化 |
+| `test` | 测试相关 |
+| `chore` | 构建/工具变更 |
+
+### 示例
+
+```
+feat(auto): 增加自动收藏功能
+fix(llm): 修复 JSON 模式下 400 错误
+docs: 更新 README 安装步骤
+refactor(mcp): 重构评论解析逻辑
+```
+
+## Pull Request 流程
+
+1. **Fork** 本仓库
+2. 从 `main` 创建特性分支:`git checkout -b feature/your-feature`
+3. 编写代码并测试
+4. 确保代码风格一致(建议使用 IDE 自动格式化)
+5. 提交更改,遵循上述提交规范
+6. 推送分支:`git push origin feature/your-feature`
+7. 在 GitHub 上发起 **Pull Request**,描述你的更改内容
+
+## Bug 报告
+
+提交 Issue 时请包含:
+
+- **环境信息**:Python 版本、操作系统、相关服务版本
+- **复现步骤**:尽可能详细的操作步骤
+- **期望行为**:你认为应该发生什么
+- **实际行为**:实际发生了什么
+- **日志/截图**:如有错误日志或截图请附上
+
+## 功能建议
+
+欢迎通过 Issue 提交功能建议,请描述:
+
+- **使用场景**:你在什么情况下需要这个功能
+- **期望功能**:你希望它如何工作
+- **参考实现**:是否有类似的项目/功能可参考
+
+## 项目架构
+
+```
+main.py # 主程序:Gradio UI + 业务逻辑 + 自动化调度
+├─ config_manager.py # 配置管理:单例模式,多 LLM 提供商
+├─ llm_service.py # LLM 封装:文案生成、热点分析、评论回复
+├─ sd_service.py # SD 封装:txt2img、img2img(JuggernautXL 优化)
+└─ mcp_client.py # MCP 客户端:小红书搜索、发布、评论、点赞
+```
+
+### 核心设计原则
+
+- **模块解耦** — 各服务独立封装,通过配置管理器共享状态
+- **MCP 协议** — 通过 JSON-RPC 与小红书 MCP 服务通信,不直接操作浏览器
+- **LLM 无关** — 支持所有 OpenAI 兼容 API,不绑定特定提供商
+- **UI 逻辑分离** — 业务函数与 Gradio UI 组件分开定义
+
+## 代码风格
+
+- Python 3.10+ 语法
+- 函数和类使用中文 docstring
+- 日志使用 `logging` 模块,不使用 `print`
+- 配置通过 `ConfigManager` 单例管理,不硬编码
+
+## 感谢
+
+感谢每一位贡献者!🙏
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..bd4d629
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 xhs-autobot contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e19c64e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,263 @@
+
+
🍒 小红书 AI 爆文生产工坊
+
+ 全自动小红书内容创作 & 运营工具
+ 灵感 → 文案 → 绘图 → 发布 → 运营,一站式全闭环
+
+
+ 功能特性 •
+ 快速开始 •
+ 使用指南 •
+ 配置说明 •
+ FAQ •
+ 贡献
+
+
+
+---
+
+## ✨ 功能特性
+
+### 📝 内容创作
+- **AI 文案生成** — 输入主题即可生成小红书爆款标题、正文、话题标签
+- **AI 绘图** — 集成 Stable Diffusion WebUI,自动生成配图(针对 JuggernautXL 优化)
+- **一键导出** — 文案 + 图片打包导出到本地文件夹
+
+### 🔥 热点探测
+- **关键词搜索** — 搜索小红书热门笔记,支持多维度排序
+- **AI 趋势分析** — 分析热门标题套路、内容结构,给出模仿建议
+- **一键借鉴创作** — 参考热门笔记风格生成原创内容
+
+### 💬 评论管家
+- **主动评论引流** — 浏览笔记 → AI 智能生成评论 → 一键发送
+- **自动回复粉丝** — 加载笔记评论 → AI 生成回复 → 发送
+
+### 🤖 自动运营(无人值守)
+- **一键评论** — 自动搜索高赞笔记 + AI 生成评论 + 发送
+- **一键点赞** — 批量随机点赞,提升账号活跃度
+- **一键回复** — 自动扫描我的笔记 + AI 回复粉丝评论
+- **一键发布** — 自动生成文案 + SD 生图 + 发布到小红书
+- **随机定时** — 评论/点赞/回复/发布全自动定时执行,随机间隔模拟真人
+
+### 📊 数据看板
+- **账号概览** — 粉丝数、获赞数等核心指标可视化
+- **笔记排行** — 点赞排行图表分析
+
+### 🔐 账号管理
+- **扫码登录** — 小红书二维码登录,自动获取 Token
+- **多 LLM 提供商** — 支持 DeepSeek、OpenAI、通义千问等所有兼容接口
+
+---
+
+## 📸 截图预览
+
+> 启动后在浏览器中打开 `http://127.0.0.1:7860`
+
+---
+
+## 🚀 快速开始
+
+### 环境要求
+
+| 依赖 | 要求 | 说明 |
+|------|------|------|
+| **Python** | >= 3.10 | 推荐 3.11+ |
+| **xiaohongshu-mcp** | 运行中 | 小红书 MCP 服务(默认端口 18060) |
+| **Stable Diffusion WebUI** | 可选 | 本地 AI 绘图(默认端口 7860) |
+| **LLM API** | 必须 | 任意 OpenAI 兼容接口 |
+
+### 安装步骤
+
+```bash
+# 1. 克隆项目
+git clone https://github.com/your-username/xhs-autobot.git
+cd xhs-autobot
+
+# 2. 创建虚拟环境(推荐)
+python -m venv .venv
+# Windows
+.venv\Scripts\activate
+# macOS/Linux
+source .venv/bin/activate
+
+# 3. 安装依赖
+pip install -r requirements.txt
+
+# 4. 复制配置文件并填写你的 API Key
+cp config.example.json config.json
+# 编辑 config.json,填写 api_key、base_url 等
+
+# 5. 启动!
+python main.py
+```
+
+启动后会自动打开浏览器,访问 `http://127.0.0.1:7860`。
+
+### 前置服务
+
+#### xiaohongshu-mcp(必须)
+
+本项目通过 [xiaohongshu-mcp](https://github.com/punkpeye/xiaohongshu-mcp) 与小红书交互(搜索、发布、评论等),请先启动 MCP 服务:
+
+```bash
+# 按照 xiaohongshu-mcp 文档启动,默认端口 18060
+npx xiaohongshu-mcp
+```
+
+#### Stable Diffusion WebUI(可选,用于 AI 绘图)
+
+推荐使用 [AUTOMATIC1111/stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui),启动时开启 API:
+
+```bash
+python launch.py --api
+```
+
+推荐模型:[JuggernautXL](https://civitai.com/models/133005)(参数已优化适配)。
+
+---
+
+## 📖 使用指南
+
+### 首次使用流程
+
+1. **配置 LLM** — 展开「⚙️ 全局设置」,添加 LLM 提供商(API Key + Base URL),点击「连接 LLM」
+2. **连接 SD**(可选)— 填写 SD WebUI URL,点击「连接 SD」
+3. **检查 MCP** — 点击「检查 MCP」确认小红书服务正常
+4. **登录小红书** — 切换到「🔐 账号登录」Tab,扫码登录
+5. **开始创作** — 切换到「✨ 内容创作」Tab,输入主题,一键生成
+
+### 自动化运营
+
+切换到「🤖 自动运营」Tab:
+
+- **一键操作** — 手动触发单次评论/点赞/回复/发布
+- **定时调度** — 勾选需要的功能,设置间隔时间,点击「▶️ 启动定时」
+- **查看日志** — 点击「🔄 刷新日志」查看实时执行记录
+
+---
+
+## ⚙️ 配置说明
+
+配置文件 `config.json` 会在运行时自动创建和保存。首次使用请从 `config.example.json` 复制:
+
+```json
+{
+ "api_key": "你的LLM API Key",
+ "base_url": "https://api.deepseek.com/v1",
+ "sd_url": "http://127.0.0.1:7860",
+ "mcp_url": "http://localhost:18060/mcp",
+ "model": "deepseek-chat",
+ "persona": "温柔知性的时尚博主",
+ "my_user_id": "你的小红书userId(24位)"
+}
+```
+
+| 字段 | 说明 | 必填 |
+|------|------|------|
+| `api_key` | LLM API 密钥 | ✅ |
+| `base_url` | LLM API 地址(OpenAI 兼容) | ✅ |
+| `sd_url` | Stable Diffusion WebUI 地址 | 绘图需要 |
+| `mcp_url` | xiaohongshu-mcp 服务地址 | ✅ |
+| `model` | 默认使用的 LLM 模型名 | ✅ |
+| `persona` | AI 评论回复的人设 | 可选 |
+| `my_user_id` | 你的小红书 userId(24 位十六进制) | 数据看板/自动回复需要 |
+| `llm_providers` | 多 LLM 提供商配置数组 | 通过 UI 管理 |
+
+### 获取 userId
+
+1. 浏览器打开你的小红书主页
+2. 网址格式为 `xiaohongshu.com/user/profile/xxxxxxxx`
+3. `profile/` 后面的就是 userId
+
+---
+
+## 📁 项目结构
+
+```
+xhs-autobot/
+├── main.py # 主程序入口 (Gradio UI + 业务逻辑)
+├── config_manager.py # 配置管理模块 (单例、自动保存)
+├── llm_service.py # LLM 服务封装 (文案生成、热点分析、评论回复)
+├── sd_service.py # Stable Diffusion 服务封装 (txt2img、img2img)
+├── mcp_client.py # 小红书 MCP 客户端 (搜索、发布、评论、点赞)
+├── config.json # 运行时配置 (gitignore)
+├── config.example.json # 配置模板
+├── requirements.txt # Python 依赖
+├── xhs_workspace/ # 导出的文案和图片 (gitignore)
+└── autobot.log # 运行日志 (gitignore)
+```
+
+---
+
+## ❓ 常见问题
+
+
+Q: LLM API 报错 400 json_object
+
+某些 API 在使用 `response_format: json_object` 时要求消息中包含 "json" 一词。本项目已自动处理,如仍遇到请升级到最新版本。
+
+
+
+Q: 评论发送成功但 App 上看不到
+
+小红书有内容审核机制,评论可能需要 1-5 分钟显示。部分评论可能被风控(仅自己可见)。查看自动化日志中的 MCP 响应可排查。
+
+
+
+Q: SD WebUI 连接失败
+
+确保启动 SD WebUI 时加了 `--api` 参数,且端口匹配。本项目默认连接 `http://127.0.0.1:7860`。
+
+
+
+Q: xiaohongshu-mcp 是什么?怎么启动?
+
+这是一个开源的小红书 MCP 服务端,提供搜索、发布、评论等 API。详见 [xiaohongshu-mcp 项目](https://github.com/punkpeye/xiaohongshu-mcp)。
+
+
+
+Q: 支持哪些 LLM?
+
+支持所有 OpenAI 兼容接口,包括但不限于:DeepSeek、GPT-4o、通义千问、Gemini(通过中转)、Claude(通过中转)等。
+
+
+---
+
+## 🤝 贡献指南
+
+欢迎贡献代码!请查看 [CONTRIBUTING.md](CONTRIBUTING.md) 了解详情。
+
+简要流程:
+1. Fork 本项目
+2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
+3. 提交更改 (`git commit -m 'feat: add amazing feature'`)
+4. 推送到分支 (`git push origin feature/amazing-feature`)
+5. 发起 Pull Request
+
+---
+
+## 📋 更新日志
+
+详见 [CHANGELOG.md](CHANGELOG.md)。
+
+---
+
+## ⚠️ 免责声明
+
+- 本项目仅供学习和研究目的,请遵守小红书平台的使用规范和服务条款
+- 过度使用自动化功能可能导致账号被限制,请合理设置操作间隔
+- 用户需为自己发布的内容和使用行为承担全部责任
+- 本项目不保存、不传输任何用户的账号密码信息
+
+---
+
+## 📄 许可证
+
+本项目使用 [MIT License](LICENSE) 开源。
+
+---
+
+## 🌟 Star History
+
+如果这个项目对你有帮助,请点亮 ⭐ Star!
+
diff --git a/config.example.json b/config.example.json
new file mode 100644
index 0000000..bd6afca
--- /dev/null
+++ b/config.example.json
@@ -0,0 +1,20 @@
+{
+ "api_key": "sk-your-api-key-here",
+ "base_url": "https://api.deepseek.com/v1",
+ "sd_url": "http://127.0.0.1:7860",
+ "mcp_url": "http://localhost:18060/mcp",
+ "model": "deepseek-chat",
+ "persona": "温柔知性的时尚博主",
+ "auto_reply_enabled": false,
+ "schedule_enabled": false,
+ "my_user_id": "",
+ "active_llm": "默认",
+ "llm_providers": [
+ {
+ "name": "默认",
+ "api_key": "sk-your-api-key-here",
+ "base_url": "https://api.deepseek.com/v1"
+ }
+ ],
+ "xsec_token": ""
+}
diff --git a/main.py b/main.py
index f6fc172..e6b11a7 100644
--- a/main.py
+++ b/main.py
@@ -1007,12 +1007,233 @@ def auto_comment_once(keywords_str, mcp_url, model, persona_text):
return f"❌ 评论失败: {e}"
+def _auto_like_with_log(keywords_str, like_count, mcp_url):
+ """一键点赞 + 同步刷新日志"""
+ msg = auto_like_once(keywords_str, like_count, mcp_url)
+ return msg, get_auto_log()
+
+
+def auto_like_once(keywords_str, like_count, mcp_url):
+ """一键点赞:搜索/推荐笔记 → 随机选择 → 批量点赞"""
+ try:
+ keywords = [k.strip() for k in keywords_str.split(",") if k.strip()] if keywords_str else DEFAULT_COMMENT_KEYWORDS
+ keyword = random.choice(keywords)
+ like_count = int(like_count) if like_count else 5
+ _auto_log_append(f"👍 点赞关键词: {keyword} | 目标: {like_count} 个")
+
+ client = get_mcp_client(mcp_url)
+
+ # 搜索笔记
+ entries = client.search_feeds_parsed(keyword, sort_by="综合")
+ if not entries:
+ _auto_log_append("⚠️ 搜索无结果,尝试推荐列表")
+ entries = client.list_feeds_parsed()
+ if not entries:
+ return "❌ 未找到任何笔记"
+
+ # 过滤自己的笔记
+ my_uid = cfg.get("my_user_id", "")
+ if my_uid:
+ filtered = [e for e in entries if e.get("user_id") != my_uid]
+ if filtered:
+ entries = filtered
+
+ # 随机打乱,取前 N 个
+ random.shuffle(entries)
+ targets = entries[:min(like_count, len(entries))]
+
+ liked = 0
+ for target in targets:
+ feed_id = target.get("feed_id", "")
+ xsec_token = target.get("xsec_token", "")
+ title = target.get("title", "未知")[:25]
+
+ if not feed_id or not xsec_token:
+ continue
+
+ # 模拟浏览延迟
+ time.sleep(random.uniform(2, 6))
+
+ result = client.like_feed(feed_id, xsec_token)
+ if "error" in result:
+ _auto_log_append(f" ❌ 点赞失败「{title}」: {result['error']}")
+ else:
+ liked += 1
+ _auto_log_append(f" ❤️ 已点赞「{title}」@{target.get('author', '未知')}")
+
+ _auto_log_append(f"👍 点赞完成: 成功 {liked}/{len(targets)}")
+ return f"✅ 点赞完成!成功 {liked}/{len(targets)} 个"
+
+ except Exception as e:
+ _auto_log_append(f"❌ 一键点赞异常: {e}")
+ return f"❌ 点赞失败: {e}"
+
+
def _auto_publish_with_log(topics_str, mcp_url, sd_url_val, sd_model_name, model):
"""一键发布 + 同步刷新日志"""
msg = auto_publish_once(topics_str, mcp_url, sd_url_val, sd_model_name, model)
return msg, get_auto_log()
+def _auto_reply_with_log(max_replies, mcp_url, model, persona_text):
+ """一键回复 + 同步刷新日志"""
+ msg = auto_reply_once(max_replies, mcp_url, model, persona_text)
+ return msg, get_auto_log()
+
+
+def auto_reply_once(max_replies, mcp_url, model, persona_text):
+ """一键回复:获取我的笔记 → 加载评论 → AI 生成回复 → 发送"""
+ try:
+ my_uid = cfg.get("my_user_id", "")
+ xsec = cfg.get("xsec_token", "")
+ if not my_uid:
+ return "❌ 未配置用户 ID,请到「账号登录」页填写"
+ if not xsec:
+ return "❌ 未获取 xsec_token,请先登录"
+
+ api_key, base_url, _ = _get_llm_config()
+ if not api_key:
+ return "❌ LLM 未配置"
+
+ max_replies = int(max_replies) if max_replies else 3
+ client = get_mcp_client(mcp_url)
+ _auto_log_append("💌 开始自动回复评论...")
+
+ # Step 1: 获取我的笔记列表
+ result = client.get_user_profile(my_uid, xsec)
+ if "error" in result:
+ _auto_log_append(f"❌ 获取我的笔记失败: {result['error']}")
+ return f"❌ 获取我的笔记失败: {result['error']}"
+
+ # 解析笔记列表
+ raw = result.get("raw", {})
+ text = result.get("text", "")
+ data = None
+ if raw and isinstance(raw, dict):
+ for item in raw.get("content", []):
+ if item.get("type") == "text":
+ try:
+ data = json.loads(item["text"])
+ except (json.JSONDecodeError, KeyError):
+ pass
+ if not data:
+ try:
+ data = json.loads(text)
+ except (json.JSONDecodeError, TypeError):
+ pass
+
+ feeds = (data or {}).get("feeds") or []
+ if not feeds:
+ _auto_log_append("⚠️ 未找到任何笔记")
+ return "⚠️ 未找到你的笔记"
+
+ # 构建笔记条目
+ my_entries = []
+ for f in feeds:
+ nc = f.get("noteCard") or {}
+ my_entries.append({
+ "feed_id": f.get("id", ""),
+ "xsec_token": f.get("xsecToken", ""),
+ "title": nc.get("displayTitle", "未知标题"),
+ })
+
+ _auto_log_append(f"📝 找到 {len(my_entries)} 篇笔记,开始扫描评论...")
+
+ # Step 2: 遍历笔记,找到未回复的评论
+ total_replied = 0
+ svc = LLMService(api_key, base_url, model)
+
+ for entry in my_entries:
+ if total_replied >= max_replies:
+ break
+
+ feed_id = entry["feed_id"]
+ xsec_token = entry["xsec_token"]
+ title = entry["title"]
+
+ if not feed_id or not xsec_token:
+ continue
+
+ time.sleep(random.uniform(1, 3))
+
+ # 加载笔记详情(含评论)
+ detail = client.get_feed_detail(feed_id, xsec_token, load_all_comments=True)
+ if "error" in detail:
+ _auto_log_append(f"⚠️ 加载「{title[:15]}」评论失败,跳过")
+ continue
+
+ full_text = detail.get("text", "")
+
+ # 解析评论
+ comments = client._parse_comments(full_text)
+ if not comments:
+ continue
+
+ # 过滤掉自己的评论,只回复他人
+ other_comments = [
+ c for c in comments
+ if c.get("user_id") and c["user_id"] != my_uid and c.get("content")
+ ]
+
+ if not other_comments:
+ continue
+
+ _auto_log_append(f"📖「{title[:20]}」有 {len(other_comments)} 条他人评论")
+
+ for comment in other_comments:
+ if total_replied >= max_replies:
+ break
+
+ comment_id = comment.get("comment_id", "")
+ comment_uid = comment.get("user_id", "")
+ comment_text = comment.get("content", "")
+ nickname = comment.get("nickname", "网友")
+
+ if not comment_text.strip():
+ continue
+
+ _auto_log_append(f" 💬 @{nickname}: {comment_text[:40]}...")
+
+ # AI 生成回复
+ try:
+ reply = svc.generate_reply(persona_text, title, comment_text)
+ except Exception as e:
+ _auto_log_append(f" ❌ AI 回复生成失败: {e}")
+ continue
+
+ _auto_log_append(f" 🤖 回复: {reply[:50]}...")
+
+ # 发送回复
+ time.sleep(random.uniform(2, 6))
+
+ if comment_id and comment_uid:
+ # 使用 reply_comment 精确回复
+ resp = client.reply_comment(
+ feed_id, xsec_token, comment_id, comment_uid, reply
+ )
+ else:
+ # 没有 comment_id 就用 post_comment 发到笔记下
+ resp = client.post_comment(feed_id, xsec_token, f"@{nickname} {reply}")
+
+ resp_text = resp.get("text", "")
+ if "error" in resp:
+ _auto_log_append(f" ❌ 回复发送失败: {resp['error']}")
+ else:
+ _auto_log_append(f" ✅ 已回复 @{nickname}")
+ total_replied += 1
+
+ if total_replied == 0:
+ _auto_log_append("ℹ️ 没有找到需要回复的新评论")
+ return "ℹ️ 没有找到需要回复的新评论\n\n💡 可能所有评论都已回复过"
+ else:
+ _auto_log_append(f"✅ 自动回复完成,共回复 {total_replied} 条评论")
+ return f"✅ 自动回复完成!共回复 {total_replied} 条评论\n\n💡 小红书审核可能有延迟,请稍后查看"
+
+ except Exception as e:
+ _auto_log_append(f"❌ 自动回复异常: {e}")
+ return f"❌ 自动回复失败: {e}"
+
+
def auto_publish_once(topics_str, mcp_url, sd_url_val, sd_model_name, model):
"""一键发布:自动生成文案 → 生成图片 → 发布到小红书"""
try:
@@ -1078,8 +1299,10 @@ def auto_publish_once(topics_str, mcp_url, sd_url_val, sd_model_name, model):
return f"❌ 发布失败: {e}"
-def _scheduler_loop(comment_enabled, publish_enabled,
+def _scheduler_loop(comment_enabled, publish_enabled, reply_enabled, like_enabled,
comment_min, comment_max, publish_min, publish_max,
+ reply_min, reply_max, max_replies_per_run,
+ like_min, like_max, like_count_per_run,
keywords, topics, mcp_url, sd_url_val, sd_model_name,
model, persona_text):
"""后台定时调度循环"""
@@ -1088,6 +1311,8 @@ def _scheduler_loop(comment_enabled, publish_enabled,
# 首次执行的随机延迟
next_comment = time.time() + random.randint(10, 60)
next_publish = time.time() + random.randint(30, 120)
+ next_reply = time.time() + random.randint(15, 90)
+ next_like = time.time() + random.randint(5, 40)
while _auto_running.is_set():
now = time.time()
@@ -1104,6 +1329,18 @@ def _scheduler_loop(comment_enabled, publish_enabled,
next_comment = time.time() + interval
_auto_log_append(f"⏰ 下次评论: {interval // 60} 分钟后")
+ # 自动点赞
+ if like_enabled and now >= next_like:
+ try:
+ _auto_log_append("--- 🔄 执行自动点赞 ---")
+ msg = auto_like_once(keywords, like_count_per_run, mcp_url)
+ _auto_log_append(msg)
+ except Exception as e:
+ _auto_log_append(f"❌ 自动点赞异常: {e}")
+ interval = random.randint(int(like_min) * 60, int(like_max) * 60)
+ next_like = time.time() + interval
+ _auto_log_append(f"⏰ 下次点赞: {interval // 60} 分钟后")
+
# 自动发布
if publish_enabled and now >= next_publish:
try:
@@ -1116,6 +1353,18 @@ def _scheduler_loop(comment_enabled, publish_enabled,
next_publish = time.time() + interval
_auto_log_append(f"⏰ 下次发布: {interval // 60} 分钟后")
+ # 自动回复评论
+ if reply_enabled and now >= next_reply:
+ try:
+ _auto_log_append("--- 🔄 执行自动回复评论 ---")
+ msg = auto_reply_once(max_replies_per_run, mcp_url, model, persona_text)
+ _auto_log_append(msg)
+ except Exception as e:
+ _auto_log_append(f"❌ 自动回复异常: {e}")
+ interval = random.randint(int(reply_min) * 60, int(reply_max) * 60)
+ next_reply = time.time() + interval
+ _auto_log_append(f"⏰ 下次回复: {interval // 60} 分钟后")
+
# 每5秒检查一次停止信号
for _ in range(5):
if not _auto_running.is_set():
@@ -1125,7 +1374,10 @@ def _scheduler_loop(comment_enabled, publish_enabled,
_auto_log_append("🛑 自动化调度器已停止")
-def start_scheduler(comment_on, publish_on, c_min, c_max, p_min, p_max,
+def start_scheduler(comment_on, publish_on, reply_on, like_on,
+ c_min, c_max, p_min, p_max, r_min, r_max,
+ max_replies_per_run,
+ l_min, l_max, like_count_per_run,
keywords, topics, mcp_url, sd_url_val, sd_model_name,
model, persona_text):
"""启动定时自动化"""
@@ -1133,18 +1385,22 @@ def start_scheduler(comment_on, publish_on, c_min, c_max, p_min, p_max,
if _auto_running.is_set():
return "⚠️ 调度器已在运行中,请先停止"
- if not comment_on and not publish_on:
- return "❌ 请至少启用一项自动化功能(评论或发布)"
+ if not comment_on and not publish_on and not reply_on and not like_on:
+ return "❌ 请至少启用一项自动化功能"
- api_key, _, _ = _get_llm_config()
- if not api_key:
- return "❌ LLM 未配置,请先在全局设置中配置提供商"
+ # 评论/回复需要 LLM,点赞不需要
+ if (comment_on or reply_on):
+ api_key, _, _ = _get_llm_config()
+ if not api_key:
+ return "❌ LLM 未配置,请先在全局设置中配置提供商"
_auto_running.set()
_auto_thread = threading.Thread(
target=_scheduler_loop,
- args=(comment_on, publish_on,
- c_min, c_max, p_min, p_max,
+ args=(comment_on, publish_on, reply_on, like_on,
+ c_min, c_max, p_min, p_max, r_min, r_max,
+ max_replies_per_run,
+ l_min, l_max, like_count_per_run,
keywords, topics, mcp_url, sd_url_val, sd_model_name,
model, persona_text),
daemon=True,
@@ -1154,8 +1410,12 @@ def start_scheduler(comment_on, publish_on, c_min, c_max, p_min, p_max,
parts = []
if comment_on:
parts.append(f"评论 (每 {int(c_min)}-{int(c_max)} 分钟)")
+ if like_on:
+ parts.append(f"点赞 (每 {int(l_min)}-{int(l_max)} 分钟, {int(like_count_per_run)}个/轮)")
if publish_on:
parts.append(f"发布 (每 {int(p_min)}-{int(p_max)} 分钟)")
+ if reply_on:
+ parts.append(f"回复 (每 {int(r_min)}-{int(r_max)} 分钟, 每轮≤{int(max_replies_per_run)}条)")
_auto_log_append(f"调度器已启动: {' + '.join(parts)}")
return f"✅ 自动化已启动 🟢\n任务: {' | '.join(parts)}\n\n💡 点击「刷新日志」查看实时进度"
@@ -1601,7 +1861,7 @@ with gr.Blocks(
with gr.Tab("🤖 自动运营"):
gr.Markdown(
"### 🤖 无人值守自动化运营\n"
- "> 一键评论引流 + 一键内容发布 + 随机定时全自动\n\n"
+ "> 一键评论引流 + 一键回复粉丝 + 一键内容发布 + 随机定时全自动\n\n"
"⚠️ **注意**: 请确保已连接 LLM、SD WebUI 和 MCP 服务"
)
@@ -1623,6 +1883,34 @@ with gr.Blocks(
)
auto_comment_result = gr.Markdown("")
+ gr.Markdown("---")
+ gr.Markdown("#### � 一键自动点赞")
+ gr.Markdown(
+ "> 搜索笔记 → 随机选择多篇 → 依次点赞\n"
+ "提升账号活跃度,无需 LLM"
+ )
+ auto_like_count = gr.Number(
+ label="单次点赞数量", value=5, minimum=1, maximum=20,
+ )
+ btn_auto_like = gr.Button(
+ "👍 一键点赞 (单次)", variant="primary", size="lg",
+ )
+ auto_like_result = gr.Markdown("")
+
+ gr.Markdown("---")
+ gr.Markdown("#### �💌 一键自动回复")
+ gr.Markdown(
+ "> 扫描我的所有笔记 → 找到粉丝评论 → AI 生成回复 → 逐条发送\n"
+ "自动跳过自己的评论,模拟真人间隔回复"
+ )
+ auto_reply_max = gr.Number(
+ label="单次最多回复条数", value=5, minimum=1, maximum=20,
+ )
+ btn_auto_reply = gr.Button(
+ "💌 一键回复 (单次)", variant="primary", size="lg",
+ )
+ auto_reply_result = gr.Markdown("")
+
gr.Markdown("---")
gr.Markdown("#### 🚀 一键智能发布")
gr.Markdown(
@@ -1659,6 +1947,36 @@ with gr.Blocks(
label="评论最大间隔(分钟)", value=45, minimum=10,
)
+ with gr.Group():
+ sched_like_on = gr.Checkbox(
+ label="✅ 启用自动点赞", value=True,
+ )
+ with gr.Row():
+ sched_l_min = gr.Number(
+ label="点赞最小间隔(分钟)", value=10, minimum=3,
+ )
+ sched_l_max = gr.Number(
+ label="点赞最大间隔(分钟)", value=30, minimum=5,
+ )
+ sched_like_count = gr.Number(
+ label="每轮点赞数量", value=5, minimum=1, maximum=15,
+ )
+
+ with gr.Group():
+ sched_reply_on = gr.Checkbox(
+ label="✅ 启用自动回复评论", value=True,
+ )
+ with gr.Row():
+ sched_r_min = gr.Number(
+ label="回复最小间隔(分钟)", value=20, minimum=5,
+ )
+ sched_r_max = gr.Number(
+ label="回复最大间隔(分钟)", value=60, minimum=10,
+ )
+ sched_reply_max = gr.Number(
+ label="每轮最多回复条数", value=3, minimum=1, maximum=10,
+ )
+
with gr.Group():
sched_publish_on = gr.Checkbox(
label="✅ 启用自动发布", value=True,
@@ -1886,6 +2204,16 @@ with gr.Blocks(
inputs=[auto_comment_keywords, mcp_url, llm_model, persona],
outputs=[auto_comment_result, auto_log_display],
)
+ btn_auto_like.click(
+ fn=_auto_like_with_log,
+ inputs=[auto_comment_keywords, auto_like_count, mcp_url],
+ outputs=[auto_like_result, auto_log_display],
+ )
+ btn_auto_reply.click(
+ fn=_auto_reply_with_log,
+ inputs=[auto_reply_max, mcp_url, llm_model, persona],
+ outputs=[auto_reply_result, auto_log_display],
+ )
btn_auto_publish.click(
fn=_auto_publish_with_log,
inputs=[auto_publish_topics, mcp_url, sd_url, sd_model, llm_model],
@@ -1893,8 +2221,10 @@ with gr.Blocks(
)
btn_start_sched.click(
fn=start_scheduler,
- inputs=[sched_comment_on, sched_publish_on,
+ inputs=[sched_comment_on, sched_publish_on, sched_reply_on, sched_like_on,
sched_c_min, sched_c_max, sched_p_min, sched_p_max,
+ sched_r_min, sched_r_max, sched_reply_max,
+ sched_l_min, sched_l_max, sched_like_count,
auto_comment_keywords, auto_publish_topics,
mcp_url, sd_url, sd_model, llm_model, persona],
outputs=[sched_result],
diff --git a/mcp_client.py b/mcp_client.py
index d5ee61b..c8b052e 100644
--- a/mcp_client.py
+++ b/mcp_client.py
@@ -272,6 +272,61 @@ class MCPClient:
return []
return self._parse_feed_entries(result.get("text", ""))
+ @staticmethod
+ def _parse_comments(text: str) -> list[dict]:
+ """从笔记详情文本中解析评论列表为结构化数据
+
+ 返回: [{comment_id, user_id, nickname, content, sub_comment_count}, ...]
+ """
+ comments = []
+
+ # 方式1: 尝试 JSON 解析
+ try:
+ data = json.loads(text)
+ raw_comments = []
+ if isinstance(data, dict):
+ raw_comments = data.get("comments", [])
+ elif isinstance(data, list):
+ raw_comments = data
+
+ for c in raw_comments:
+ user_info = c.get("userInfo") or c.get("user") or {}
+ comments.append({
+ "comment_id": c.get("id", c.get("commentId", "")),
+ "user_id": user_info.get("userId", user_info.get("user_id", "")),
+ "nickname": user_info.get("nickname", user_info.get("nickName", "未知")),
+ "content": c.get("content", ""),
+ "sub_comment_count": c.get("subCommentCount", 0),
+ })
+ if comments:
+ return comments
+ except (json.JSONDecodeError, TypeError, AttributeError):
+ pass
+
+ # 方式2: 正则提取 —— 适配多种 MCP 文本格式
+ # 格式举例: "评论ID: xxx | 用户: xxx (userId) | 内容: xxx"
+ # 或者: 用户名(@nickname): 评论内容
+ comment_ids = re.findall(
+ r'(?:comment_?[Ii]d|评论ID|评论id|"id")["\s::]+([0-9a-f]{24})', text, re.I)
+ user_ids = re.findall(
+ r'(?:user_?[Ii]d|userId|用户ID)["\s::]+([0-9a-f]{24})', text, re.I)
+ nicknames = re.findall(
+ r'(?:nickname|昵称|用户名|用户)["\s::]+([^\n|,]{1,30})', text, re.I)
+ contents = re.findall(
+ r'(?:content|内容|评论内容)["\s::]+([^\n]{1,500})', text, re.I)
+
+ count = max(len(comment_ids), len(contents))
+ for i in range(count):
+ comments.append({
+ "comment_id": comment_ids[i] if i < len(comment_ids) else "",
+ "user_id": user_ids[i] if i < len(user_ids) else "",
+ "nickname": (nicknames[i].strip() if i < len(nicknames) else ""),
+ "content": (contents[i].strip() if i < len(contents) else ""),
+ "sub_comment_count": 0,
+ })
+
+ return comments
+
# ---------- 帖子详情 ----------
def get_feed_detail(self, feed_id: str, xsec_token: str,