feat(auto): 新增自动点赞和自动回复功能,完善项目文档

- 新增自动点赞功能【一键点赞】:支持关键词搜索笔记并随机批量点赞,提升账号活跃度
- 新增自动回复功能【一键回复】:自动扫描用户笔记的粉丝评论,使用AI生成并发送回复
- 扩展自动化调度器【定时调度】:支持点赞和回复任务的随机定时执行,模拟真人操作间隔
- 新增项目文档【文档】:添加README、CHANGELOG、CONTRIBUTING、LICENSE等核心文档文件
- 优化.gitignore文件【配置】:完善Python项目、IDE、敏感文件、日志等忽略规则
- 新增配置文件模板【配置】:提供config.example.json作为配置参考
- 优化MCP客户端【工具】:新增评论解析方法,支持从笔记详情中提取结构化评论数据
This commit is contained in:
zhoujie 2026-02-08 22:40:16 +08:00
parent d27ffe94f4
commit d782bb6781
8 changed files with 914 additions and 14 deletions

46
.gitignore vendored
View File

@ -1,3 +1,45 @@
xhs_workspace
__pycache__
# ========== 工作空间 ==========
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

51
CHANGELOG.md Normal file
View File

@ -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

118
CONTRIBUTING.md Normal file
View File

@ -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>(<scope>): <description>
[可选正文]
[可选脚注]
```
### 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、img2imgJuggernautXL 优化)
└─ mcp_client.py # MCP 客户端:小红书搜索、发布、评论、点赞
```
### 核心设计原则
- **模块解耦** — 各服务独立封装,通过配置管理器共享状态
- **MCP 协议** — 通过 JSON-RPC 与小红书 MCP 服务通信,不直接操作浏览器
- **LLM 无关** — 支持所有 OpenAI 兼容 API不绑定特定提供商
- **UI 逻辑分离** — 业务函数与 Gradio UI 组件分开定义
## 代码风格
- Python 3.10+ 语法
- 函数和类使用中文 docstring
- 日志使用 `logging` 模块,不使用 `print`
- 配置通过 `ConfigManager` 单例管理,不硬编码
## 感谢
感谢每一位贡献者!🙏

21
LICENSE Normal file
View File

@ -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.

263
README.md Normal file
View File

@ -0,0 +1,263 @@
<p align="center">
<h1 align="center">🍒 小红书 AI 爆文生产工坊</h1>
<p align="center">
<strong>全自动小红书内容创作 & 运营工具</strong><br>
灵感 → 文案 → 绘图 → 发布 → 运营,一站式全闭环
</p>
<p align="center">
<a href="#功能特性">功能特性</a>
<a href="#快速开始">快速开始</a>
<a href="#使用指南">使用指南</a>
<a href="#配置说明">配置说明</a>
<a href="#常见问题">FAQ</a>
<a href="#贡献指南">贡献</a>
</p>
</p>
---
## ✨ 功能特性
### 📝 内容创作
- **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` | 你的小红书 userId24 位十六进制) | 数据看板/自动回复需要 |
| `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)
```
---
## ❓ 常见问题
<details>
<summary><b>Q: LLM API 报错 400 json_object</b></summary>
某些 API 在使用 `response_format: json_object` 时要求消息中包含 "json" 一词。本项目已自动处理,如仍遇到请升级到最新版本。
</details>
<details>
<summary><b>Q: 评论发送成功但 App 上看不到</b></summary>
小红书有内容审核机制,评论可能需要 1-5 分钟显示。部分评论可能被风控(仅自己可见)。查看自动化日志中的 MCP 响应可排查。
</details>
<details>
<summary><b>Q: SD WebUI 连接失败</b></summary>
确保启动 SD WebUI 时加了 `--api` 参数,且端口匹配。本项目默认连接 `http://127.0.0.1:7860`
</details>
<details>
<summary><b>Q: xiaohongshu-mcp 是什么?怎么启动?</b></summary>
这是一个开源的小红书 MCP 服务端,提供搜索、发布、评论等 API。详见 [xiaohongshu-mcp 项目](https://github.com/punkpeye/xiaohongshu-mcp)。
</details>
<details>
<summary><b>Q: 支持哪些 LLM</b></summary>
支持所有 OpenAI 兼容接口包括但不限于DeepSeek、GPT-4o、通义千问、Gemini通过中转、Claude通过中转等。
</details>
---
## 🤝 贡献指南
欢迎贡献代码!请查看 [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

20
config.example.json Normal file
View File

@ -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": ""
}

352
main.py
View File

@ -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("#### <20> 一键自动点赞")
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("#### <20>💌 一键自动回复")
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],

View File

@ -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,