736 lines
23 KiB
Python
736 lines
23 KiB
Python
"""
|
||
小红书控制技能 - 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}
|