Files
workphone-sdk/sdk/agent/skills/xhs/skill.py

736 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
小红书控制技能 - 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}