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

617 lines
19 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. 视频互动 - 点赞、收藏、分享
@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 DouyinSkill(BaseSkill):
"""
抖音控制技能
支持的操作:
- send_message: 发送私信
- get_messages: 获取私信列表
- get_fans: 获取粉丝列表
- follow_user: 关注用户
- unfollow_user: 取消关注
- get_comments: 获取视频评论
- reply_comment: 回复评论
- like_video: 点赞视频
- collect_video: 收藏视频
- search_user: 搜索用户
- batch_send_message: 批量发送私信
"""
PACKAGE = "com.ss.android.ugc.aweme"
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_text("搜索") or self.click_desc("搜索"):
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("发送")
logger.info(f"抖音私信发送成功: {to_id}")
return {
"success": True,
"message_id": f"dy_{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_text("搜索") or self.click_desc("搜索"):
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, video_id: str = None, limit: int = 50) -> Dict[str, Any]:
"""
获取视频评论
Args:
video_id: 视频ID如果不指定获取当前视频的评论
limit: 评论数量
"""
try:
self.launch()
self.wait_for_app_ready()
if video_id and str(video_id).strip():
self.click_text("搜索") or self.click_desc("搜索")
self.sleep(0.8)
self.input_text(str(video_id).strip()[:50])
self.sleep(1.5)
if self.click_contains(str(video_id).strip()[:20]) or self.click_text("搜索"):
self.sleep(1)
# 点击评论按钮
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,
video_id: str,
comment_id: str,
content: str
) -> Dict[str, Any]:
"""
回复评论
Args:
video_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_video(self, video_id: str = None) -> Dict[str, Any]:
"""
点赞视频
Args:
video_id: 视频ID不指定则点赞当前视频
"""
try:
self.launch()
self.wait_for_app_ready()
# 双击屏幕点赞
info = self.d.info
center_x = info['displayWidth'] // 2
center_y = info['displayHeight'] // 2
self.d.double_click(center_x, center_y)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
def collect_video(self, video_id: str = None) -> Dict[str, Any]:
"""
收藏视频
Args:
video_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_video(self, video_id: str = None, platform: str = "wechat") -> Dict[str, Any]:
"""
分享视频
Args:
video_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_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}