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,