1325 lines
40 KiB
Python
1325 lines
40 KiB
Python
"""
|
||
工作手机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_execute;ADB 模式由服务端解析后执行 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": "设备离线"}
|