""" 小红书控制技能 - Agent端实现 功能模块: 1. 消息管理 - 发送/获取私信 2. 粉丝管理 - 获取粉丝列表、关注用户 3. 评论管理 - 获取评论、回复评论 4. 笔记互动 - 点赞、收藏、分享 5. 笔记发布 - 发布图文笔记 @author 卡若 @version 3.0.0 """ import time import logging import re from typing import Dict, Any, List, Optional import sys import os _agent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) if _agent_dir not in sys.path: sys.path.insert(0, _agent_dir) from skills.base import BaseSkill from error_handler import retry, with_error_handling logger = logging.getLogger(__name__) class XhsSkill(BaseSkill): """ 小红书控制技能 支持的操作: - send_message: 发送私信 - get_messages: 获取私信列表 - get_fans: 获取粉丝列表 - follow_user: 关注用户 - unfollow_user: 取消关注 - get_comments: 获取笔记评论 - reply_comment: 回复评论 - like_note: 点赞笔记 - collect_note: 收藏笔记 - search_user: 搜索用户 - search_note: 搜索笔记 - batch_send_message: 批量发送私信 - post_note: 发布笔记 """ PACKAGE = "com.xingin.xhs" NAME = "小红书" # ========================================================================== # 一、消息管理 # ========================================================================== @with_error_handling("发送小红书私信") @retry(max_retries=2, delay=0.5) def send_message(self, to_id: str, content: str, msg_type: str = "text") -> Dict[str, Any]: """ 发送私信 Args: to_id: 用户ID或昵称 content: 消息内容 msg_type: 消息类型 (text/image) Returns: 执行结果 """ try: self.launch() self.wait_for_app_ready() # 进入消息页面 self.click_text("消息") or self.click_desc("消息") self.sleep(1) # 搜索用户 if self.click_desc("搜索") or self.click_text("搜索"): self.sleep(0.5) self.input_text(to_id) self.sleep(1.5) # 点击搜索结果 if not self.click_text(to_id): if not self.click_contains(to_id): return {"success": False, "error": f"未找到用户: {to_id}"} self.sleep(1) # 确保在私信界面 if not self.exists("发送") and not self.exists("输入"): return {"success": False, "error": "未能进入私信界面"} # 输入消息 self.input_text(content, clear=False) self.sleep(0.3) # 发送 self.click_text("发送") or self.click_desc("发送") logger.info(f"小红书私信发送成功: {to_id}") return { "success": True, "message_id": f"xhs_{int(time.time() * 1000)}" } except Exception as e: logger.error(f"小红书发送失败: {e}") return {"success": False, "error": str(e)} def get_messages(self, limit: int = 20, conversation_id: str = None) -> Dict[str, Any]: """ 获取私信列表 Args: limit: 消息数量限制 conversation_id: 会话ID(用户昵称) Returns: 消息列表 """ try: self.launch() self.wait_for_app_ready() # 进入消息页面 self.click_text("消息") or self.click_desc("消息") self.sleep(1) messages = [] if conversation_id: # 搜索进入特定会话 if self.click_desc("搜索") or self.click_text("搜索"): self.sleep(0.5) self.input_text(conversation_id) self.sleep(1.5) self.click_text(conversation_id) or self.click_contains(conversation_id) self.sleep(1) # 获取UI树解析消息 ui_tree = self.d.dump_hierarchy() import xml.etree.ElementTree as ET try: root = ET.fromstring(ui_tree) for elem in root.iter(): text = elem.get("text", "") if text and len(text) > 0: if "发送" not in text and "输入" not in text: messages.append({ "text": text, "timestamp": int(time.time() * 1000) }) except: pass return { "success": True, "messages": messages[:limit], "count": len(messages[:limit]) } except Exception as e: return {"success": False, "error": str(e), "messages": []} def batch_send_message( self, to_ids: List[str], content: str, interval: float = 3.0 ) -> Dict[str, Any]: """批量发送私信""" results = [] success_count = 0 failed_count = 0 for to_id in to_ids: try: result = self.send_message(to_id, content) if result.get("success"): success_count += 1 else: failed_count += 1 results.append({ "to_id": to_id, "success": result.get("success", False), "error": result.get("error"), "message_id": result.get("message_id") }) time.sleep(interval) except Exception as e: failed_count += 1 results.append({ "to_id": to_id, "success": False, "error": str(e) }) return { "success": True, "success_count": success_count, "failed_count": failed_count, "results": results } # ========================================================================== # 二、粉丝管理 # ========================================================================== def get_fans(self, limit: int = 100) -> Dict[str, Any]: """ 获取粉丝列表 Args: limit: 获取数量 Returns: 粉丝列表 """ try: self.launch() self.wait_for_app_ready() # 进入我的页面 self.click_text("我") or self.click_desc("我") self.sleep(1) # 点击粉丝 self.click_text("粉丝") self.sleep(1) fans = [] ui_tree = self.d.dump_hierarchy() import xml.etree.ElementTree as ET try: root = ET.fromstring(ui_tree) for elem in root.iter(): text = elem.get("text", "") if text and len(text) > 0: if "粉丝" not in text and "关注" not in text: fans.append({"name": text}) except: pass return { "success": True, "fans": fans[:limit], "count": len(fans[:limit]) } except Exception as e: return {"success": False, "error": str(e), "fans": []} def follow_user(self, user_id: str) -> Dict[str, Any]: """ 关注用户 Args: user_id: 用户ID或昵称 """ try: self.launch() self.wait_for_app_ready() # 搜索用户 self._search_user(user_id) # 点击关注 if self.click_text("关注"): return {"success": True} # 可能已关注 if self.exists("已关注") or self.exists("互相关注"): return {"success": True, "message": "已关注"} return {"success": False, "error": "关注失败"} except Exception as e: return {"success": False, "error": str(e)} def unfollow_user(self, user_id: str) -> Dict[str, Any]: """取消关注""" try: self.launch() self.wait_for_app_ready() # 搜索用户 self._search_user(user_id) # 点击已关注 if self.click_text("已关注") or self.click_text("互相关注"): self.sleep(0.5) # 确认取消关注 self.click_text("取消关注") return {"success": True} return {"success": False, "error": "取消关注失败"} except Exception as e: return {"success": False, "error": str(e)} def search_user(self, keyword: str, limit: int = 20) -> Dict[str, Any]: """ 搜索用户 Args: keyword: 搜索关键词 limit: 结果数量 """ try: self.launch() self.wait_for_app_ready() # 点击搜索 self.click_desc("搜索") self.sleep(0.5) # 输入关键词 self.input_text(keyword) self.sleep(1) # 切换到用户标签 self.click_text("用户") self.sleep(1) # 获取搜索结果 users = [] ui_tree = self.d.dump_hierarchy() import xml.etree.ElementTree as ET try: root = ET.fromstring(ui_tree) for elem in root.iter(): text = elem.get("text", "") if text and len(text) > 0: if "用户" not in text and "搜索" not in text: users.append({"name": text}) except: pass return { "success": True, "users": users[:limit], "count": len(users[:limit]) } except Exception as e: return {"success": False, "error": str(e), "users": []} # ========================================================================== # 三、评论管理 # ========================================================================== def get_comments(self, note_id: str = None, limit: int = 50) -> Dict[str, Any]: """ 获取笔记评论 Args: note_id: 笔记ID(如果不指定,获取当前笔记的评论) limit: 评论数量 """ try: self.launch() self.wait_for_app_ready() # 点击评论按钮 self.click_desc("评论") or self.click_text("评论") self.sleep(1) comments = [] ui_tree = self.d.dump_hierarchy() import xml.etree.ElementTree as ET try: root = ET.fromstring(ui_tree) for elem in root.iter(): text = elem.get("text", "") if text and len(text) > 5: comments.append({ "content": text, "timestamp": int(time.time() * 1000) }) except: pass return { "success": True, "comments": comments[:limit], "count": len(comments[:limit]) } except Exception as e: return {"success": False, "error": str(e), "comments": []} def reply_comment( self, note_id: str, comment_id: str, content: str ) -> Dict[str, Any]: """ 回复评论 Args: note_id: 笔记ID comment_id: 评论ID或评论内容片段 content: 回复内容 """ try: self.launch() self.wait_for_app_ready() # 打开评论区 self.click_desc("评论") or self.click_text("评论") self.sleep(1) # 找到评论并点击回复 if self.click_contains(comment_id): self.sleep(0.5) self.click_text("回复") self.sleep(0.3) # 输入回复内容 self.input_text(content) self.sleep(0.3) # 发送 self.click_text("发送") return {"success": True} return {"success": False, "error": "未找到评论"} except Exception as e: return {"success": False, "error": str(e)} # ========================================================================== # 四、笔记互动 # ========================================================================== def like_note(self, note_id: str = None) -> Dict[str, Any]: """ 点赞笔记 Args: note_id: 笔记ID(不指定则点赞当前笔记) """ try: self.launch() self.wait_for_app_ready() # 点击点赞按钮 self.click_desc("点赞") or self.click_text("点赞") or self.click_desc("赞") return {"success": True} except Exception as e: return {"success": False, "error": str(e)} def collect_note(self, note_id: str = None) -> Dict[str, Any]: """ 收藏笔记 Args: note_id: 笔记ID """ try: self.launch() self.wait_for_app_ready() # 点击收藏按钮 self.click_desc("收藏") or self.click_text("收藏") return {"success": True} except Exception as e: return {"success": False, "error": str(e)} def share_note(self, note_id: str = None, platform: str = "wechat") -> Dict[str, Any]: """ 分享笔记 Args: note_id: 笔记ID platform: 分享平台 (wechat/qq/weibo) """ try: self.launch() self.wait_for_app_ready() # 点击分享按钮 self.click_desc("分享") or self.click_text("分享") self.sleep(1) # 选择平台 platform_map = { "wechat": "微信", "qq": "QQ", "weibo": "微博" } target = platform_map.get(platform, platform) self.click_text(target) or self.click_contains(target) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} def search_note(self, keyword: str, limit: int = 20) -> Dict[str, Any]: """ 搜索笔记 Args: keyword: 搜索关键词 limit: 结果数量 """ try: self.launch() self.wait_for_app_ready() # 点击搜索 self.click_desc("搜索") self.sleep(0.5) # 输入关键词 self.input_text(keyword) self.sleep(1) # 切换到笔记标签 self.click_text("笔记") or self.click_text("全部") self.sleep(1) # 获取搜索结果 notes = [] ui_tree = self.d.dump_hierarchy() import xml.etree.ElementTree as ET try: root = ET.fromstring(ui_tree) for elem in root.iter(): text = elem.get("text", "") if text and len(text) > 10: notes.append({"title": text}) except: pass return { "success": True, "notes": notes[:limit], "count": len(notes[:limit]) } except Exception as e: return {"success": False, "error": str(e), "notes": []} # ========================================================================== # 五、笔记发布 # ========================================================================== def post_note( self, content: str, images: List[str] = None, title: str = None, topics: List[str] = None, location: str = None ) -> Dict[str, Any]: """ 发布笔记 Args: content: 笔记内容 images: 图片路径列表 title: 笔记标题 topics: 话题标签列表 location: 位置信息 Returns: 发布结果 """ try: self.launch() self.wait_for_app_ready() # 点击发布按钮(通常在底部中间) self.click_text("+") or self.click_desc("发布") self.sleep(1) # 选择图文笔记 self.click_text("图文") or self.click_text("笔记") self.sleep(1) # 选择图片(如果有) if images: for img in images: # TODO: 实现图片选择逻辑 pass self.sleep(1) self.click_text("下一步") or self.click_text("完成") self.sleep(1) # 输入标题(如果有) if title: self.click_text("标题") or self.click_text("添加标题") self.input_text(title) # 输入正文 self.click_text("正文") or self.click_text("添加正文") self.input_text(content) self.sleep(0.5) # 添加话题标签 if topics: for topic in topics: self.input_text(f" #{topic}") # 添加位置 if location: self.click_text("位置") or self.click_text("添加位置") self.sleep(0.5) self.input_text(location) self.sleep(1) self.click_contains(location) # 发布 self.click_text("发布笔记") or self.click_text("发布") self.sleep(2) return { "success": True, "note_id": f"xhs_note_{int(time.time() * 1000)}" } except Exception as e: return {"success": False, "error": str(e)} # ========================================================================== # 六、辅助方法 # ========================================================================== def _search_user(self, user_id: str): """搜索用户(内部方法)""" # 点击搜索 self.click_desc("搜索") self.sleep(0.5) # 输入用户ID self.input_text(user_id) self.sleep(1) # 切换到用户标签 self.click_text("用户") self.sleep(1) # 点击第一个结果 self.click_text(user_id) or self.click_contains(user_id) self.sleep(1) def get_contacts(self, limit: int = 100) -> Dict[str, Any]: """获取最近联系人列表(与微信技能接口保持一致)""" try: self.launch() self.wait_for_app_ready() # 进入消息页面 self.click_text("消息") or self.click_desc("消息") self.sleep(1) contacts = [] ui_tree = self.d.dump_hierarchy() import xml.etree.ElementTree as ET try: root = ET.fromstring(ui_tree) for elem in root.iter(): text = elem.get("text", "") if text and len(text) > 0: if "消息" not in text and "搜索" not in text: contacts.append({"name": text}) except: pass return { "success": True, "contacts": contacts[:limit], "count": len(contacts[:limit]) } except Exception as e: return {"success": False, "error": str(e), "contacts": []} def add_friend(self, user_id: str, message: str = "") -> Dict[str, Any]: """添加好友(关注用户)- 与微信技能接口保持一致""" return self.follow_user(user_id) def accept_friend(self, user_id: str = None) -> Dict[str, Any]: """通过好友请求 - 小红书无此入口,占位""" return {"success": False, "error": "小红书暂不支持通过好友请求"} def set_remark(self, user_id: str, remark: str) -> Dict[str, Any]: """设置备注 - 小红书无此能力,占位""" return {"success": False, "error": "小红书暂不支持设置备注"} def delete_friend(self, user_id: str) -> Dict[str, Any]: """删除好友 - 小红书为取消关注,占位""" return self.unfollow_user(user_id) def batch_add_friend(self, user_ids: List[str], message: str = "", interval: float = 5.0) -> Dict[str, Any]: """批量关注用户""" results = [] success_count = 0 failed_count = 0 for uid in user_ids: try: r = self.add_friend(uid, message) ok = r.get("success", False) if ok: success_count += 1 else: failed_count += 1 results.append({"user_id": uid, "success": ok, "error": r.get("error")}) time.sleep(max(interval, 2.0)) except Exception as e: failed_count += 1 results.append({"user_id": uid, "success": False, "error": str(e)}) return {"success_count": success_count, "failed_count": failed_count, "results": results}