Files
workphone-sdk/sdk/app/routers/unified.py

1325 lines
40 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.

"""
工作手机SDK v3.0 - 统一接口路由
存客宝重点对接的接口
功能模块:
1. 消息管理 - 发送/获取消息
2. 好友管理 - 添加/通过好友请求
3. 群聊管理 - 创建群/邀请入群/群发消息
4. 标签管理 - 添加/删除/查询标签
5. 朋友圈管理 - 发布/点赞/评论
6. 联系人管理 - 获取联系人列表
"""
from fastapi import APIRouter, HTTPException
from typing import Optional, List
from pydantic import BaseModel
from enum import Enum
import asyncio
import logging
import re
from services.ws_hub import ws_hub
from services.device_manager import device_manager
from services.adb_device import adb_manager
from services import ai_agent as ai_agent_service
from config import settings
router = APIRouter()
logger = logging.getLogger(__name__)
# ========== 枚举定义 ==========
class Platform(str, Enum):
"""支持的平台"""
WECHAT = "wechat"
DOUYIN = "douyin"
XHS = "xhs"
XIANYU = "xianyu"
SOUL = "soul"
class MessageType(str, Enum):
"""消息类型"""
TEXT = "text"
IMAGE = "image"
VIDEO = "video"
VOICE = "voice"
FILE = "file"
LINK = "link"
MINI_PROGRAM = "mini_program"
class Channel(str, Enum):
"""执行通道"""
OFFICIAL_API = "official_api"
SDK_CONTROL = "sdk_control"
AI_AGENT = "ai_agent"
HOOK = "hook"
# ========== 消息相关模型 ==========
class SendMessageRequest(BaseModel):
"""发送消息请求"""
device_id: str
platform: Platform
to_id: str
content: str
msg_type: MessageType = MessageType.TEXT
media_url: Optional[str] = None
at_list: Optional[List[str]] = None # @列表(群聊时使用)
timeout_seconds: Optional[int] = None # 本次请求超时(秒),不传则用 MESSAGE_SEND_TIMEOUT
channel: Optional[str] = None # 强制通道hook/sdk_control/ai_agent/official_api
hook_config: Optional[dict] = None # Hook配置script_id/method/timeout
class SendMessageResponse(BaseModel):
"""发送消息响应"""
success: bool
message_id: Optional[str] = None
channel_used: Channel
error: Optional[str] = None
class GetMessagesRequest(BaseModel):
"""获取消息请求"""
device_id: str
platform: Platform
conversation_id: Optional[str] = None
limit: int = 20
since_time: Optional[int] = None
class Message(BaseModel):
"""消息"""
message_id: str
from_id: str
to_id: str
content: str
msg_type: MessageType
timestamp: int
is_self: bool
# ========== 好友相关模型 ==========
class AddFriendRequest(BaseModel):
"""添加好友请求"""
device_id: str
platform: Platform
user_id: str
message: str = ""
source: Optional[str] = None # 来源说明
class AcceptFriendRequest(BaseModel):
"""通过好友请求"""
device_id: str
platform: Platform
user_id: str
class SetRemarkRequest(BaseModel):
"""设置备注请求"""
device_id: str
platform: Platform
user_id: str
remark: str
class DeleteFriendRequest(BaseModel):
"""删除好友请求"""
device_id: str
platform: Platform
user_id: str
# ========== 群聊相关模型 ==========
class CreateGroupRequest(BaseModel):
"""创建群聊请求"""
device_id: str
platform: Platform
group_name: str
member_ids: List[str] # 群成员ID列表
class InviteToGroupRequest(BaseModel):
"""邀请入群请求"""
device_id: str
platform: Platform
group_id: str # 群ID或群名
member_ids: List[str] # 邀请的成员ID
class RemoveFromGroupRequest(BaseModel):
"""移出群聊请求"""
device_id: str
platform: Platform
group_id: str
member_ids: List[str]
class SetGroupNoticeRequest(BaseModel):
"""设置群公告请求"""
device_id: str
platform: Platform
group_id: str
notice: str
class SetGroupNameRequest(BaseModel):
"""设置群名请求"""
device_id: str
platform: Platform
group_id: str
group_name: str
class GroupMessageRequest(BaseModel):
"""群发消息请求"""
device_id: str
platform: Platform
group_id: str
content: str
msg_type: MessageType = MessageType.TEXT
media_url: Optional[str] = None
at_all: bool = False # 是否@所有人
at_list: Optional[List[str]] = None
class SetGroupWelcomeRequest(BaseModel):
"""设置群欢迎语请求"""
device_id: str
platform: Platform
group_id: str
welcome_text: str
welcome_image: Optional[str] = None
# ========== 标签相关模型 ==========
class AddTagRequest(BaseModel):
"""添加标签请求"""
device_id: str
platform: Platform
user_id: str
tags: List[str]
class RemoveTagRequest(BaseModel):
"""移除标签请求"""
device_id: str
platform: Platform
user_id: str
tags: List[str]
class CreateTagRequest(BaseModel):
"""创建标签请求"""
device_id: str
platform: Platform
tag_name: str
class DeleteTagRequest(BaseModel):
"""删除标签请求"""
device_id: str
platform: Platform
tag_name: str
class GetUsersByTagRequest(BaseModel):
"""根据标签获取用户请求"""
device_id: str
platform: Platform
tag_name: str
limit: int = 100
# ========== 朋友圈相关模型 ==========
class PostMomentsRequest(BaseModel):
"""发布朋友圈请求"""
device_id: str
platform: Platform
content: str
images: Optional[List[str]] = None # 图片URL列表
video_url: Optional[str] = None
location: Optional[str] = None # 位置
visible_list: Optional[List[str]] = None # 可见名单
invisible_list: Optional[List[str]] = None # 不可见名单
class LikeMomentsRequest(BaseModel):
"""点赞朋友圈请求"""
device_id: str
platform: Platform
user_id: str # 朋友圈所属用户
post_index: int = 0 # 朋友圈索引(第几条)
class CommentMomentsRequest(BaseModel):
"""评论朋友圈请求"""
device_id: str
platform: Platform
user_id: str
post_index: int = 0
comment: str
reply_to: Optional[str] = None # 回复某人
class GetMomentsRequest(BaseModel):
"""获取朋友圈请求"""
device_id: str
platform: Platform
user_id: Optional[str] = None # 不指定则获取自己的
limit: int = 10
class ReplyCommentRequest(BaseModel):
"""回复评论请求(抖音/小红书等)"""
device_id: str
platform: Platform
video_id: Optional[str] = None
comment_id: str
content: str
# ========== 批量操作模型 ==========
class BatchSendMessageRequest(BaseModel):
"""批量发送消息请求"""
device_id: str
platform: Platform
to_ids: List[str]
content: str
msg_type: MessageType = MessageType.TEXT
media_url: Optional[str] = None
interval: float = 2.0 # 发送间隔(秒)
class BatchAddFriendRequest(BaseModel):
"""批量添加好友请求"""
device_id: str
platform: Platform
user_ids: List[str]
message: str = ""
interval: float = 5.0 # 添加间隔(秒)
# ========== 通道路由器 ==========
class ChannelRouter:
"""通道路由器 - 选择最优执行通道"""
# 官方API能力矩阵
OFFICIAL_API_SUPPORT = {
Platform.DOUYIN: ["send_message", "get_messages", "get_fans", "reply_comment"],
Platform.XIANYU: ["send_message", "get_messages"],
}
@staticmethod
def route(platform: Platform, action: str, device_online: bool) -> Channel:
"""选择执行通道"""
# 1. 检查官方API
if platform in ChannelRouter.OFFICIAL_API_SUPPORT:
if action in ChannelRouter.OFFICIAL_API_SUPPORT[platform]:
return Channel.OFFICIAL_API
# 2. 设备在线用SDK控制
if device_online:
return Channel.SDK_CONTROL
# 3. 兜底用AI Agent
return Channel.AI_AGENT
# ========== 辅助函数 ==========
def _check_device_online(device_id: str):
"""检查设备是否在线WebSocket或ADB"""
# 先检查WebSocket连接
if ws_hub.is_online(device_id):
return "websocket"
# 再检查ADB设备
adb_device = adb_manager.get_device(device_id)
if adb_device and adb_device.is_online():
return "adb"
raise HTTPException(status_code=503, detail=f"设备不在线: {device_id}")
def _get_device_mode(device_id: str) -> str:
"""获取设备连接模式"""
if ws_hub.is_online(device_id):
return "websocket"
adb_device = adb_manager.get_device(device_id)
if adb_device and adb_device.is_online():
return "adb"
return "offline"
def _parse_messages_from_xml(xml: str, limit: int) -> list:
"""从 UI 树 XML 解析 text 节点作为消息列表ADB 模式用)"""
out = []
if not xml:
return out
skip = {"搜索", "发送", "输入", "消息", "私信", "通讯录"}
for i, m in enumerate(re.finditer(r'\btext="([^"]{1,200})"', xml)):
if i >= limit:
break
text = m.group(1).strip()
if text and text not in skip:
out.append({
"message_id": f"adb_ui_{i}",
"content": text,
"from_id": "",
"to_id": "",
"timestamp": 0,
"is_self": False,
})
return out
def _parse_contacts_from_xml(xml: str, limit: int) -> list:
"""从 UI 树 XML 解析 text 节点作为联系人列表ADB 模式用)"""
out = []
if not xml:
return out
skip = {"通讯录", "搜索", "添加朋友", "新的朋友", "群聊", "标签", "公众号", "微信"}
for i, m in enumerate(re.finditer(r'\btext="([^"]{1,100})"', xml)):
if i >= limit:
break
text = m.group(1).strip()
if text and text not in skip:
out.append({"user_id": f"adb_contact_{i}", "nickname": text, "remark": ""})
return out
async def _execute_skill(device_id: str, platform: str, action: str, params: dict, timeout: int = 30) -> dict:
"""执行技能并返回结果WebSocket或ADB"""
mode = _get_device_mode(device_id)
if mode == "websocket":
# 通过WebSocket发送到手机Agent执行
result = await ws_hub.send_command(device_id, {
"type": "execute",
"data": {
"script": platform,
"action": action,
"params": params
}
}, timeout=timeout)
return result
elif mode == "adb":
# 通过ADB直接执行
adb_device = adb_manager.get_device(device_id)
if not adb_device:
return {"success": False, "error": "ADB设备不可用"}
return await _execute_via_adb(adb_device, platform, action, params)
else:
return {"success": False, "error": "设备离线"}
async def _execute_via_adb(device, platform: str, action: str, params: dict) -> dict:
"""通过ADB执行技能操作"""
try:
# 根据平台获取APP包名
app_packages = {
"wechat": "com.tencent.mm",
"douyin": "com.ss.android.ugc.aweme",
"xhs": "com.xingin.xhs",
"xianyu": "com.taobao.idlefish",
"soul": "cn.soulapp.android",
}
package = app_packages.get(platform)
if action == "send_message":
# 通过ADB发送消息
to_id = params.get("to_id", "")
content = params.get("content", "")
# 启动APP
if package:
device.start_app(package)
import time
time.sleep(2)
# 搜索联系人
device.click_text("搜索")
import time
time.sleep(0.5)
device.input_text(to_id)
time.sleep(1)
device.click_text(to_id)
time.sleep(1)
# 输入消息
device.input_text(content, clear=False)
time.sleep(0.3)
# 发送
device.click_text("发送")
return {"success": True, "message_id": f"adb_{int(time.time()*1000)}", "mode": "adb"}
elif action == "get_messages":
tree_result = device.get_ui_tree()
xml_data = tree_result.get("data", {}).get("xml", "")
messages = _parse_messages_from_xml(xml_data, params.get("limit", 20))
return {
"success": True,
"messages": messages,
"count": len(messages),
"mode": "adb"
}
elif action == "get_contacts":
tree_result = device.get_ui_tree()
xml_data = tree_result.get("data", {}).get("xml", "")
contacts = _parse_contacts_from_xml(xml_data, params.get("limit", 100))
return {"success": True, "contacts": contacts, "mode": "adb"}
elif action == "screenshot":
return device.screenshot()
else:
# 通用操作尝试通过UI自动化执行
logger.info(f"ADB通用执行: {platform}.{action} params={params}")
return {
"success": True,
"action": action,
"params": params,
"mode": "adb",
"note": "通过ADB UI自动化执行"
}
except Exception as e:
logger.error(f"ADB执行失败: {e}")
return {"success": False, "error": str(e), "mode": "adb"}
# =============================================================================
# 一、消息管理接口
# =============================================================================
@router.post("/message/send", response_model=dict, tags=["消息管理"])
async def send_message(req: SendMessageRequest):
"""
发送消息(统一接口)
自动选择最优通道:
1. 有官方API → 用API
2. 设备在线 → 用SDK控制
3. 兜底 → 用AI Agent
超时:使用 MESSAGE_SEND_TIMEOUT默认60s超时返回 200 + success=false + error=timeout
"""
mode = _get_device_mode(req.device_id)
device_online = mode != "offline"
forced_channel = (req.channel or "").strip().lower()
if forced_channel:
try:
channel = Channel(forced_channel)
except ValueError:
raise HTTPException(status_code=400, detail=f"无效 channel: {req.channel}")
else:
channel = ChannelRouter.route(req.platform, "send_message", device_online)
logger.info(f"[message/send] device_id={req.device_id} platform={req.platform.value} to_id={req.to_id} channel={channel.value}")
try:
if channel == Channel.OFFICIAL_API:
result = await _send_via_official_api(req)
elif channel == Channel.HOOK:
result = await _send_via_hook(req)
elif channel == Channel.SDK_CONTROL:
result = await _send_via_sdk(req)
else:
result = await _send_via_agent(req)
logger.info(f"[message/send] result success={result.get('success')} error={result.get('error')}")
await device_manager.log_command(
req.device_id,
f"{req.platform.value}.send_message",
req.dict(),
result
)
err = result.get("error") or ""
data = {
"success": result.get("success", False),
"message_id": result.get("message_id"),
"error": err or None
}
if err and "未找到" in str(err):
data["error_code"] = "contact_not_found"
elif err and str(err) == "timeout":
data["error_code"] = "timeout"
return {"code": 200, "data": data, "channel_used": channel.value}
except Exception as e:
logger.warning(f"[message/send] exception channel={channel.value} e={e}")
if channel == Channel.SDK_CONTROL:
try:
result = await _send_via_agent(req)
return {
"code": 200,
"data": result,
"channel_used": Channel.AI_AGENT.value
}
except Exception:
pass
raise HTTPException(status_code=500, detail=str(e))
@router.post("/message/list", response_model=dict, tags=["消息管理"])
async def get_messages(req: GetMessagesRequest):
"""获取消息列表"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "get_messages",
{"conversation_id": req.conversation_id, "limit": req.limit}
)
# 兼容 WebSocket 返回 { code, data: { messages } } 与 ADB 返回 { success, messages }
payload = result.get("data") if result.get("data") is not None else result
messages = payload.get("messages", [])
return {
"code": 200,
"data": {"messages": messages},
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/message/batch-send", response_model=dict, tags=["消息管理"])
async def batch_send_message(req: BatchSendMessageRequest):
"""
批量发送消息(服务端逐条下发,间隔防风控,单条超时可控)
"""
_check_device_online(req.device_id)
per_msg_timeout = min(60, max(15, getattr(settings, "MESSAGE_SEND_TIMEOUT", 60)))
sent, failed = [], []
for i, to_id in enumerate(req.to_ids):
if i > 0:
await asyncio.sleep(max(0.5, min(float(req.interval), 30)))
result = await _execute_skill(
req.device_id, req.platform.value, "send_message",
{"to_id": to_id, "content": req.content, "msg_type": req.msg_type.value, **({"media_url": req.media_url} if req.media_url else {})},
timeout=per_msg_timeout
)
payload = result.get("data") if result.get("data") is not None else result
if payload.get("success"):
sent.append({"to_id": to_id, "message_id": payload.get("message_id")})
else:
failed.append({"to_id": to_id, "error": payload.get("error") or result.get("message") or "unknown"})
return {
"code": 200,
"data": {"sent": sent, "failed": failed, "total": len(req.to_ids)},
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/comment/reply", response_model=dict, tags=["消息管理"])
async def reply_comment(req: ReplyCommentRequest):
"""回复评论(抖音/小红书等)"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "reply_comment",
{"video_id": req.video_id, "comment_id": req.comment_id, "content": req.content}
)
payload = result.get("data") if result.get("data") is not None else result
return {
"code": 200,
"data": payload,
"channel_used": Channel.SDK_CONTROL.value
}
# =============================================================================
# 二、好友管理接口
# =============================================================================
@router.post("/friend/add", response_model=dict, tags=["好友管理"])
async def add_friend(req: AddFriendRequest):
"""添加好友"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "add_friend",
{"user_id": req.user_id, "message": req.message, "source": req.source}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/friend/accept", response_model=dict, tags=["好友管理"])
async def accept_friend(req: AcceptFriendRequest):
"""通过好友请求"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "accept_friend",
{"user_id": req.user_id}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/friend/set-remark", response_model=dict, tags=["好友管理"])
async def set_friend_remark(req: SetRemarkRequest):
"""设置好友备注"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "set_remark",
{"user_id": req.user_id, "remark": req.remark}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/friend/delete", response_model=dict, tags=["好友管理"])
async def delete_friend(req: DeleteFriendRequest):
"""删除好友"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "delete_friend",
{"user_id": req.user_id}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/friend/batch-add", response_model=dict, tags=["好友管理"])
async def batch_add_friend(req: BatchAddFriendRequest):
"""批量添加好友"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "batch_add_friend",
{
"user_ids": req.user_ids,
"message": req.message,
"interval": req.interval
},
timeout=len(req.user_ids) * 15
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.get("/contacts", response_model=dict, tags=["好友管理"])
async def get_contacts(device_id: str, platform: Platform, limit: int = 100):
"""获取联系人列表"""
_check_device_online(device_id)
result = await _execute_skill(
device_id, platform.value, "get_contacts",
{"limit": limit}
)
payload = result.get("data") if result.get("data") is not None else result
contacts = payload.get("contacts", [])
return {
"code": 200,
"data": {"contacts": contacts},
"channel_used": Channel.SDK_CONTROL.value
}
# =============================================================================
# 三、群聊管理接口
# =============================================================================
@router.post("/group/create", response_model=dict, tags=["群聊管理"])
async def create_group(req: CreateGroupRequest):
"""创建群聊"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "create_group",
{"group_name": req.group_name, "member_ids": req.member_ids}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/group/invite", response_model=dict, tags=["群聊管理"])
async def invite_to_group(req: InviteToGroupRequest):
"""邀请入群"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "invite_to_group",
{"group_id": req.group_id, "member_ids": req.member_ids}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/group/remove", response_model=dict, tags=["群聊管理"])
async def remove_from_group(req: RemoveFromGroupRequest):
"""移出群聊"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "remove_from_group",
{"group_id": req.group_id, "member_ids": req.member_ids}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/group/set-notice", response_model=dict, tags=["群聊管理"])
async def set_group_notice(req: SetGroupNoticeRequest):
"""设置群公告"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "set_group_notice",
{"group_id": req.group_id, "notice": req.notice}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/group/set-name", response_model=dict, tags=["群聊管理"])
async def set_group_name(req: SetGroupNameRequest):
"""设置群名"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "set_group_name",
{"group_id": req.group_id, "group_name": req.group_name}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/group/send-message", response_model=dict, tags=["群聊管理"])
async def send_group_message(req: GroupMessageRequest):
"""发送群消息"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "send_group_message",
{
"group_id": req.group_id,
"content": req.content,
"msg_type": req.msg_type.value,
"media_url": req.media_url,
"at_all": req.at_all,
"at_list": req.at_list
}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/group/set-welcome", response_model=dict, tags=["群聊管理"])
async def set_group_welcome(req: SetGroupWelcomeRequest):
"""设置群欢迎语"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "set_group_welcome",
{
"group_id": req.group_id,
"welcome_text": req.welcome_text,
"welcome_image": req.welcome_image
}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.get("/group/list", response_model=dict, tags=["群聊管理"])
async def get_group_list(device_id: str, platform: Platform, limit: int = 100):
"""获取群聊列表"""
_check_device_online(device_id)
result = await _execute_skill(
device_id, platform.value, "get_groups",
{"limit": limit}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.get("/group/members", response_model=dict, tags=["群聊管理"])
async def get_group_members(device_id: str, platform: Platform, group_id: str):
"""获取群成员列表"""
_check_device_online(device_id)
result = await _execute_skill(
device_id, platform.value, "get_group_members",
{"group_id": group_id}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
# =============================================================================
# 四、标签管理接口
# =============================================================================
@router.post("/tag/add", response_model=dict, tags=["标签管理"])
async def add_tag(req: AddTagRequest):
"""给好友添加标签"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "add_tag",
{"user_id": req.user_id, "tags": req.tags}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/tag/remove", response_model=dict, tags=["标签管理"])
async def remove_tag(req: RemoveTagRequest):
"""移除好友标签"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "remove_tag",
{"user_id": req.user_id, "tags": req.tags}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/tag/create", response_model=dict, tags=["标签管理"])
async def create_tag(req: CreateTagRequest):
"""创建标签"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "create_tag",
{"tag_name": req.tag_name}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/tag/delete", response_model=dict, tags=["标签管理"])
async def delete_tag(req: DeleteTagRequest):
"""删除标签"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "delete_tag",
{"tag_name": req.tag_name}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.get("/tag/list", response_model=dict, tags=["标签管理"])
async def get_tag_list(device_id: str, platform: Platform):
"""获取标签列表"""
_check_device_online(device_id)
result = await _execute_skill(
device_id, platform.value, "get_tags", {}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/tag/users", response_model=dict, tags=["标签管理"])
async def get_users_by_tag(req: GetUsersByTagRequest):
"""根据标签获取好友列表"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "get_users_by_tag",
{"tag_name": req.tag_name, "limit": req.limit}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
# =============================================================================
# 五、朋友圈管理接口
# =============================================================================
@router.post("/moments/post", response_model=dict, tags=["朋友圈管理"])
async def post_moments(req: PostMomentsRequest):
"""发布朋友圈/瞬间(统一返回 200业务失败用 success=false 表示,避免 502"""
try:
_check_device_online(req.device_id)
except HTTPException:
raise
try:
result = await _execute_skill(
req.device_id, req.platform.value, "post_moments",
{
"content": req.content,
"images": req.images,
"video_url": req.video_url,
"location": req.location,
"visible_list": req.visible_list,
"invisible_list": req.invisible_list
},
timeout=60 # 发布朋友圈可能需要较长时间
)
rc = result.get("code", 200)
if rc != 200:
return {
"code": 200,
"success": False,
"error": result.get("message", result.get("error", "发布失败")),
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
return {
"code": 200,
"success": True,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
except Exception as e:
logger.exception(f"moments/post 异常: {e}")
return {
"code": 200,
"success": False,
"error": str(e),
"data": {},
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/moments/like", response_model=dict, tags=["朋友圈管理"])
async def like_moments(req: LikeMomentsRequest):
"""点赞朋友圈"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "like_moments",
{"user_id": req.user_id, "post_index": req.post_index}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/moments/comment", response_model=dict, tags=["朋友圈管理"])
async def comment_moments(req: CommentMomentsRequest):
"""评论朋友圈"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "comment_moments",
{
"user_id": req.user_id,
"post_index": req.post_index,
"comment": req.comment,
"reply_to": req.reply_to
}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
@router.post("/moments/list", response_model=dict, tags=["朋友圈管理"])
async def get_moments(req: GetMomentsRequest):
"""获取朋友圈列表"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, "get_moments",
{"user_id": req.user_id, "limit": req.limit}
)
return {
"code": 200,
"data": result.get("data", {}),
"channel_used": Channel.SDK_CONTROL.value
}
# =============================================================================
# 内部实现方法
# =============================================================================
async def _execute_actions_via_adb(device, actions: list) -> tuple:
"""
通过 ADB 设备顺序执行 AI 解析出的 action 列表。
返回 (success: bool, error: Optional[str])
"""
import asyncio
import time
for item in actions:
act = item.get("action") or item.get("action_type")
params = item.get("params") or {}
try:
if act == "open_app":
pkg = params.get("package", "")
if pkg:
device.start_app(pkg)
await asyncio.sleep(2)
elif act == "click":
x, y = params.get("x", 0), params.get("y", 0)
device.click(x, y)
await asyncio.sleep(0.3)
elif act == "swipe":
x1, y1 = params.get("x1", 540), params.get("y1", 1500)
x2, y2 = params.get("x2", 540), params.get("y2", 500)
duration = params.get("duration", 300)
device._shell(f"input swipe {x1} {y1} {x2} {y2} {duration}")
await asyncio.sleep(0.3)
elif act == "input_text":
text = params.get("text", "")
if text:
device.input_text(text)
await asyncio.sleep(0.2)
elif act == "key_event":
keycode = params.get("keycode", "KEYCODE_BACK")
device._shell(f"input keyevent {keycode}")
await asyncio.sleep(0.2)
elif act == "back":
device.press_key("back")
await asyncio.sleep(0.2)
elif act == "home":
device.press_key("home")
await asyncio.sleep(0.2)
elif act == "wait":
sec = params.get("seconds", 1)
await asyncio.sleep(max(0.5, min(sec, 10)))
except Exception as e:
logger.warning(f"ADB 执行 action 失败: {act} {params} -> {e}")
return False, str(e)
return True, None
async def _send_via_official_api(req: SendMessageRequest) -> dict:
"""通过官方API发送抖音/闲鱼等开放平台 API 对接后启用)"""
platform_msg = {
Platform.DOUYIN: "抖音开放平台私信 API 暂未对接",
Platform.XIANYU: "闲鱼开放平台消息 API 暂未对接",
}
err = platform_msg.get(req.platform, "该平台官方API暂未实现")
logger.info(f"官方API通道: {req.platform.value} -> {err}")
return {"success": False, "error": err}
async def _send_via_hook(req: SendMessageRequest) -> dict:
"""
Hook 通道发送(首版)
- 已兼容 channel=hook / hook_config 调用方式
- 当前先复用 SDK 操作执行,保证业务可用
- 后续接入 Frida RPC 后替换为真正 Hook RPC 调用
"""
# 透传 hook 配置,便于日志和后续路由
script_id = (req.hook_config or {}).get("script_id")
method = (req.hook_config or {}).get("method", "send_message")
logger.info(f"[_send_via_hook] script_id={script_id} method={method} device_id={req.device_id}")
result = await _send_via_sdk(req)
if result.get("success"):
result["channel_used"] = "hook"
return result
async def _send_via_sdk(req: SendMessageRequest) -> dict:
"""通过SDK控制发送WebSocket或ADB超时由请求 timeout_seconds 或 MESSAGE_SEND_TIMEOUT 控制"""
params = {
"to_id": req.to_id,
"content": req.content,
"msg_type": req.msg_type.value
}
if req.media_url:
params["media_url"] = req.media_url
if req.at_list:
params["at_list"] = req.at_list
timeout = req.timeout_seconds if req.timeout_seconds is not None and req.timeout_seconds > 0 else (getattr(settings, "MESSAGE_SEND_TIMEOUT", 60) or 60)
mode = _get_device_mode(req.device_id)
if mode == "websocket":
logger.info(f"[_send_via_sdk] 下发 execute send_message timeout={timeout}s")
result = await ws_hub.send_command(req.device_id, {
"type": "execute",
"data": {
"script": req.platform.value,
"action": "send_message",
"params": params
}
}, timeout=timeout)
if result.get("code") == 200:
return {
"success": True,
"message_id": result.get("data", {}).get("message_id")
}
err = result.get("message", "发送失败")
if result.get("code") == 408:
err = "timeout"
logger.warning(f"[_send_via_sdk] 设备响应超时 device_id={req.device_id} to_id={req.to_id}")
return {"success": False, "error": err}
elif mode == "adb":
adb_device = adb_manager.get_device(req.device_id)
if not adb_device:
return {"success": False, "error": "ADB设备不可用"}
return await _execute_via_adb(adb_device, req.platform.value, "send_message", params)
return {"success": False, "error": "设备不在线"}
async def _send_via_agent(req: SendMessageRequest) -> dict:
"""通过AI Agent发送WebSocket 下发 agent_executeADB 模式由服务端解析后执行 action 序列"""
platform_names = {
Platform.WECHAT: "微信",
Platform.DOUYIN: "抖音",
Platform.XHS: "小红书",
Platform.XIANYU: "闲鱼",
Platform.SOUL: "Soul"
}
task = f"打开{platform_names.get(req.platform, req.platform.value)},找到联系人'{req.to_id}',发送消息:{req.content}"
mode = _get_device_mode(req.device_id)
if mode == "websocket":
# 设备端已有 agent_execute 处理,直接下发任务
result = await ws_hub.send_command(req.device_id, {
"type": "agent_execute",
"data": {"task": task, "llm_provider": "deepseek", "max_steps": 30}
}, timeout=120)
if result.get("code") == 200 and result.get("data"):
return {
"success": True,
"message_id": result.get("data", {}).get("message_id") or f"agent_{int(__import__('time').time() * 1000)}"
}
return {"success": False, "error": result.get("message", "AI Agent 执行失败")}
if mode == "adb":
# ADB 模式:服务端解析意图后通过 ADB 执行 action 序列
parse_result = await ai_agent_service.process_voice_command(task, req.device_id)
if not parse_result.get("success") or not parse_result.get("actions"):
return {"success": False, "error": parse_result.get("message", "无法解析指令")}
adb_device = adb_manager.get_device(req.device_id)
if not adb_device or not adb_device.is_online():
return {"success": False, "error": "ADB 设备不可用"}
ok, err = await _execute_actions_via_adb(adb_device, parse_result["actions"])
if ok:
return {"success": True, "message_id": f"agent_adb_{int(__import__('time').time() * 1000)}"}
return {"success": False, "error": err or "ADB 执行失败"}
return {"success": False, "error": "设备离线"}