2350 lines
89 KiB
Python
2350 lines
89 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, Field
|
||
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 services.rate_limiter import rate_limiter, DailyLimitExceeded
|
||
from services.content_guard import diversify_content, filter_sensitive, has_sensitive_words
|
||
from services.account_lifecycle import account_lifecycle
|
||
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):
|
||
"""检查设备是否在线(按优先级:Hook > WebSocket > ADB)"""
|
||
from services.connection_priority import connection_priority
|
||
best = connection_priority.get_best_mode(device_id)
|
||
if best:
|
||
return best.to_dict()["id"] # hook / agent / adb
|
||
raise HTTPException(status_code=503, detail=f"设备不在线: {device_id}")
|
||
|
||
|
||
def _get_device_mode(device_id: str) -> str:
|
||
"""获取设备最优连接模式"""
|
||
from services.connection_priority import connection_priority
|
||
channel = connection_priority.choose_execution_channel(device_id)
|
||
if channel == "agent":
|
||
return "websocket"
|
||
return channel
|
||
|
||
|
||
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 _anti_ban_guard(device_id: str, platform: str, action: str, content: Optional[str] = None) -> dict:
|
||
"""
|
||
防封守卫:在执行任何操作前进行防封检查。
|
||
返回 {"pass": True/False, "reason": str, "content": str(处理后的内容)}
|
||
检查链:时段 → 生命周期 → L1全局 → L2设备 → L3动作 → 敏感词 → 内容差异化
|
||
"""
|
||
from services.anti_ban_alert import (
|
||
alert_daily_limit, alert_outside_hours, alert_sensitive_content,
|
||
)
|
||
from services.rate_limiter import OutsideOperationHours
|
||
|
||
try:
|
||
lifecycle_check = await account_lifecycle.check_allowed(device_id, platform, action)
|
||
if not lifecycle_check["allowed"]:
|
||
return {"pass": False, "reason": lifecycle_check["reason"], "content": content}
|
||
except Exception as e:
|
||
logger.warning(f"[anti_ban] 生命周期检查异常(跳过): {e}")
|
||
|
||
try:
|
||
is_new = await account_lifecycle.is_new_account(device_id, platform)
|
||
wait = await rate_limiter.check_and_wait(device_id, platform, action, is_new_account=is_new)
|
||
if wait > 0:
|
||
logger.info(f"[anti_ban] {device_id}/{platform}.{action} 等待了 {wait:.1f}s")
|
||
except OutsideOperationHours as e:
|
||
asyncio.ensure_future(alert_outside_hours(device_id, action))
|
||
return {"pass": False, "reason": str(e), "content": content}
|
||
except DailyLimitExceeded as e:
|
||
asyncio.ensure_future(alert_daily_limit(device_id, platform, action, -1, -1))
|
||
return {"pass": False, "reason": str(e), "content": content}
|
||
except Exception as e:
|
||
logger.warning(f"[anti_ban] 限流检查异常(跳过): {e}")
|
||
|
||
safe_content = content
|
||
if content:
|
||
sensitive = has_sensitive_words(content)
|
||
if sensitive:
|
||
logger.warning(f"[anti_ban] 检测到敏感词: {sensitive}")
|
||
asyncio.ensure_future(alert_sensitive_content(device_id, platform, sensitive))
|
||
safe_content = diversify_content(content, add_invisible=True, add_emoji=False)
|
||
|
||
return {"pass": True, "reason": "", "content": safe_content}
|
||
|
||
|
||
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执行技能操作 — 微信走专用引擎,其他平台走通用 UI 自动化"""
|
||
try:
|
||
if platform == "wechat":
|
||
from services.wechat_adb_engine import WeChatADBEngine
|
||
engine = WeChatADBEngine(device)
|
||
loop = asyncio.get_event_loop()
|
||
result = await loop.run_in_executor(None, lambda: engine.execute(action, params))
|
||
return result
|
||
|
||
app_packages = {
|
||
"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 == "screenshot":
|
||
return device.screenshot()
|
||
|
||
if action == "send_message":
|
||
import time as _time
|
||
to_id = params.get("to_id", "")
|
||
content = params.get("content", "")
|
||
if package:
|
||
device.start_app(package)
|
||
_time.sleep(2)
|
||
device.click_text("搜索")
|
||
_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"}
|
||
|
||
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"}
|
||
|
||
|
||
# =============================================================================
|
||
# 〇、Hawk Hook 统一执行入口(丝滑控制)
|
||
# =============================================================================
|
||
|
||
class HookExecuteRequest(BaseModel):
|
||
"""Hawk Hook 格式 — 单端点执行任意操作,任何应用可直连"""
|
||
device_id: str
|
||
platform: Platform = Platform.WECHAT
|
||
action: str # send_message | get_contacts | post_moments | ... 共 96 个
|
||
params: dict = Field(default_factory=dict, description="action 所需参数")
|
||
|
||
|
||
@router.post("/hook/execute", response_model=dict, tags=["Hook模块管理"])
|
||
async def hook_execute(req: HookExecuteRequest):
|
||
"""
|
||
Hawk Hook 统一执行 — 单接口控制整台手机
|
||
|
||
任意应用只需调此端点,传 action + params 即可执行 96 种操作。
|
||
支持 WebSocket(Agent) 与 ADB 双通道,执行更丝滑。
|
||
|
||
示例:
|
||
```json
|
||
{"device_id":"xgfe65eimrrofyws","platform":"wechat","action":"send_message","params":{"to_id":"阿猫","content":"你好"}}
|
||
```
|
||
"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, req.action, req.params or {}
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": "sdk_control"}
|
||
|
||
|
||
# =============================================================================
|
||
# 一、消息管理接口
|
||
# =============================================================================
|
||
|
||
@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
|
||
"""
|
||
# ---- 防封守卫 ----
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "send_message", req.content)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
|
||
req.content = guard["content"]
|
||
|
||
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):
|
||
"""
|
||
批量发送消息(服务端逐条下发,间隔防风控,单条超时可控)
|
||
"""
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "batch_send", req.content)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
|
||
req.content = guard["content"]
|
||
|
||
_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)
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "comment", req.content)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "reply_comment",
|
||
{"video_id": req.video_id, "comment_id": req.comment_id, "content": guard.get("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):
|
||
"""添加好友"""
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "add_friend", req.message)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
|
||
req.message = guard["content"] or req.message
|
||
|
||
_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)
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "add_friend", req.message)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "batch_add_friend",
|
||
{
|
||
"user_ids": req.user_ids,
|
||
"message": guard.get("content", 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)
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "send_group_message", req.content)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "send_group_message",
|
||
{
|
||
"group_id": req.group_id,
|
||
"content": guard.get("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)"""
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "post_moments", req.content)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "success": False, "error": guard["reason"], "data": {}, "channel_used": "none"}
|
||
req.content = guard["content"]
|
||
|
||
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)
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "like_moments")
|
||
if not guard["pass"]:
|
||
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
|
||
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)
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "comment_moments", req.comment)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "comment_moments",
|
||
{
|
||
"user_id": req.user_id,
|
||
"post_index": req.post_index,
|
||
"comment": guard.get("content", 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
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 六、消息高级操作
|
||
# =============================================================================
|
||
|
||
class ForwardMessageRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
to_id: str
|
||
content: str
|
||
|
||
class SendCardRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
to_id: str
|
||
card_wxid: str
|
||
|
||
@router.post("/message/forward", response_model=dict, tags=["消息管理"])
|
||
async def forward_message(req: ForwardMessageRequest):
|
||
"""转发消息"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "forward_message",
|
||
{"to_id": req.to_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("/message/recall", response_model=dict, tags=["消息管理"])
|
||
async def recall_message(device_id: str, platform: Platform):
|
||
"""撤回最近一条消息"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "recall_message", {})
|
||
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("/message/send-card", response_model=dict, tags=["消息管理"])
|
||
async def send_card(req: SendCardRequest):
|
||
"""发送名片"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "send_card",
|
||
{"to_id": req.to_id, "card_wxid": req.card_wxid}
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
|
||
|
||
|
||
# =============================================================================
|
||
# 七、个人设置接口
|
||
# =============================================================================
|
||
|
||
class SetNicknameRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
nickname: str
|
||
|
||
class SetSignatureRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
signature: str
|
||
|
||
class SetAvatarRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
image_path: Optional[str] = None
|
||
|
||
class SetGenderRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
gender: str
|
||
|
||
class SetRegionRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
region: str
|
||
|
||
@router.get("/profile/get", response_model=dict, tags=["个人设置"])
|
||
async def get_profile(device_id: str, platform: Platform):
|
||
"""获取当前微信账号资料"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "get_profile", {})
|
||
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("/profile/set-nickname", response_model=dict, tags=["个人设置"])
|
||
async def set_nickname(req: SetNicknameRequest):
|
||
"""修改微信昵称"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "set_nickname",
|
||
{"nickname": req.nickname}
|
||
)
|
||
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("/profile/set-signature", response_model=dict, tags=["个人设置"])
|
||
async def set_signature(req: SetSignatureRequest):
|
||
"""修改个性签名"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "set_signature",
|
||
{"signature": req.signature}
|
||
)
|
||
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("/profile/set-avatar", response_model=dict, tags=["个人设置"])
|
||
async def set_avatar(req: SetAvatarRequest):
|
||
"""修改头像"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "set_avatar",
|
||
{"image_path": req.image_path}
|
||
)
|
||
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("/profile/set-gender", response_model=dict, tags=["个人设置"])
|
||
async def set_gender(req: SetGenderRequest):
|
||
"""设置性别"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "set_gender",
|
||
{"gender": req.gender}
|
||
)
|
||
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("/profile/set-region", response_model=dict, tags=["个人设置"])
|
||
async def set_region(req: SetRegionRequest):
|
||
"""设置地区"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "set_region",
|
||
{"region": req.region}
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
|
||
|
||
|
||
# =============================================================================
|
||
# 八、账号安全接口
|
||
# =============================================================================
|
||
|
||
class UnblockAccountRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
helper_wxid: Optional[str] = None
|
||
|
||
class ChangePasswordRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
old_pwd: str
|
||
new_pwd: str
|
||
|
||
@router.get("/account/status", response_model=dict, tags=["账号安全"])
|
||
async def check_account_status(device_id: str, platform: Platform):
|
||
"""检查账号状态"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "check_account_status", {})
|
||
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("/account/unblock", response_model=dict, tags=["账号安全"])
|
||
async def unblock_account(req: UnblockAccountRequest):
|
||
"""微信解封"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "unblock_account",
|
||
{"helper_wxid": req.helper_wxid}
|
||
)
|
||
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.get("/account/safety-center", response_model=dict, tags=["账号安全"])
|
||
async def safety_center(device_id: str, platform: Platform):
|
||
"""打开微信安全中心"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "safety_center", {})
|
||
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("/account/change-password", response_model=dict, tags=["账号安全"])
|
||
async def change_password(req: ChangePasswordRequest):
|
||
"""修改微信密码"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "change_password",
|
||
{"old_pwd": req.old_pwd, "new_pwd": req.new_pwd}
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
|
||
|
||
|
||
# =============================================================================
|
||
# 九、收藏管理接口
|
||
# =============================================================================
|
||
|
||
class AddFavoriteRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
content_desc: Optional[str] = None
|
||
|
||
@router.post("/favorites/add", response_model=dict, tags=["收藏管理"])
|
||
async def add_favorite(req: AddFavoriteRequest):
|
||
"""收藏消息"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "add_to_favorites",
|
||
{"content_desc": req.content_desc}
|
||
)
|
||
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.get("/favorites/list", response_model=dict, tags=["收藏管理"])
|
||
async def get_favorites(device_id: str, platform: Platform, limit: int = 20):
|
||
"""获取收藏列表"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "get_favorites", {"limit": limit})
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
|
||
|
||
|
||
# =============================================================================
|
||
# 十、聊天设置接口
|
||
# =============================================================================
|
||
|
||
class ChatSettingRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
user_id: str
|
||
enable: bool = True
|
||
|
||
@router.post("/chat/set-top", response_model=dict, tags=["聊天设置"])
|
||
async def set_chat_top(req: ChatSettingRequest):
|
||
"""置顶/取消置顶聊天"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "set_chat_top",
|
||
{"user_id": req.user_id, "enable": req.enable}
|
||
)
|
||
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("/chat/set-mute", response_model=dict, tags=["聊天设置"])
|
||
async def set_mute_chat(req: ChatSettingRequest):
|
||
"""消息免打扰"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "set_mute_chat",
|
||
{"user_id": req.user_id, "enable": req.enable}
|
||
)
|
||
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("/chat/clear-history", response_model=dict, tags=["聊天设置"])
|
||
async def clear_chat_history(device_id: str, platform: Platform, user_id: str):
|
||
"""清空聊天记录"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(
|
||
device_id, platform.value, "clear_chat_history",
|
||
{"user_id": user_id}
|
||
)
|
||
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.get("/contacts/search", response_model=dict, tags=["好友管理"])
|
||
async def search_contacts(device_id: str, platform: Platform, keyword: str):
|
||
"""搜索联系人"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(
|
||
device_id, platform.value, "search_contact",
|
||
{"keyword": keyword}
|
||
)
|
||
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.get("/friend/info", response_model=dict, tags=["好友管理"])
|
||
async def get_friend_info(device_id: str, platform: Platform, user_id: str):
|
||
"""获取好友详细资料"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(
|
||
device_id, platform.value, "get_friend_info",
|
||
{"user_id": user_id}
|
||
)
|
||
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("/group/quit", response_model=dict, tags=["群聊管理"])
|
||
async def quit_group(device_id: str, platform: Platform, group_id: str):
|
||
"""退出群聊"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(
|
||
device_id, platform.value, "quit_group",
|
||
{"group_id": group_id}
|
||
)
|
||
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("/moments/delete", response_model=dict, tags=["朋友圈管理"])
|
||
async def delete_moments(device_id: str, platform: Platform, post_index: int = 0):
|
||
"""删除朋友圈"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(
|
||
device_id, platform.value, "delete_moments",
|
||
{"post_index": post_index}
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
|
||
|
||
|
||
# =============================================================================
|
||
# 十四、支付接口
|
||
# =============================================================================
|
||
|
||
class RedPacketRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
to_id: str
|
||
amount: str
|
||
message: str = "恭喜发财"
|
||
|
||
class TransferRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
to_id: str
|
||
amount: str
|
||
message: str = ""
|
||
|
||
@router.post("/payment/red-packet", response_model=dict, tags=["支付"])
|
||
async def send_red_packet(req: RedPacketRequest):
|
||
"""发红包"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "send_red_packet",
|
||
{"to_id": req.to_id, "amount": req.amount, "message": req.message}
|
||
)
|
||
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("/payment/transfer", response_model=dict, tags=["支付"])
|
||
async def send_transfer(req: TransferRequest):
|
||
"""转账"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "transfer",
|
||
{"to_id": req.to_id, "amount": req.amount, "message": req.message}
|
||
)
|
||
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("/miniprogram/open", response_model=dict, tags=["小程序"])
|
||
async def open_mini_program(device_id: str, platform: Platform, name: str):
|
||
"""打开小程序"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(
|
||
device_id, platform.value, "open_mini_program", {"name": name}
|
||
)
|
||
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("/official-account/follow", response_model=dict, tags=["公众号"])
|
||
async def follow_official_account(device_id: str, platform: Platform, account_name: str):
|
||
"""关注公众号"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(
|
||
device_id, platform.value, "follow_official_account",
|
||
{"account_name": account_name}
|
||
)
|
||
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("/account/unblock-self", response_model=dict, tags=["账号安全"])
|
||
async def unblock_self(device_id: str, platform: Platform):
|
||
"""自助解封"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "unblock_self", {})
|
||
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("/account/unblock-appeal", response_model=dict, tags=["账号安全"])
|
||
async def unblock_appeal(device_id: str, platform: Platform):
|
||
"""申诉解封"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "unblock_appeal", {})
|
||
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.get("/account/restrictions", response_model=dict, tags=["账号安全"])
|
||
async def check_restrictions(device_id: str, platform: Platform):
|
||
"""检查当前功能限制"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "check_restrictions", {})
|
||
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("/account/appeal-restriction", response_model=dict, tags=["账号安全"])
|
||
async def appeal_restriction(device_id: str, platform: Platform):
|
||
"""申诉功能限制"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "appeal_restriction", {})
|
||
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("/account/unblock-sms", response_model=dict, tags=["账号安全"])
|
||
async def unblock_with_sms(device_id: str, platform: Platform, phone: str = ""):
|
||
"""短信验证解封"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "unblock_with_sms", {"phone": phone})
|
||
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.get("/video-channel/list", response_model=dict, tags=["视频号"])
|
||
async def get_video_list(device_id: str, platform: Platform, limit: int = 10):
|
||
"""获取视频号列表"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "get_video_list", {"limit": limit})
|
||
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("/video-channel/like", response_model=dict, tags=["视频号"])
|
||
async def like_video(device_id: str, platform: Platform, index: int = 0):
|
||
"""点赞视频"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "like_video", {"index": index})
|
||
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("/video-channel/comment", response_model=dict, tags=["视频号"])
|
||
async def comment_video(device_id: str, platform: Platform, index: int = 0, comment: str = ""):
|
||
"""评论视频"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "comment_video", {"index": index, "comment": comment})
|
||
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("/video-channel/follow", response_model=dict, tags=["视频号"])
|
||
async def follow_video_creator(device_id: str, platform: Platform, index: int = 0):
|
||
"""关注视频号创作者"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "follow_video_creator", {"index": index})
|
||
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("/video-channel/share", response_model=dict, tags=["视频号"])
|
||
async def share_video(device_id: str, platform: Platform, index: int = 0, to_id: str = ""):
|
||
"""分享视频"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "share_video", {"index": index, "to_id": to_id})
|
||
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("/scan/qr-code", response_model=dict, tags=["扫一扫"])
|
||
async def scan_qr_code(device_id: str, platform: Platform):
|
||
"""扫描二维码"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "scan_qr_code", {})
|
||
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("/scan/add-friend", response_model=dict, tags=["扫一扫"])
|
||
async def scan_add_friend(device_id: str, platform: Platform):
|
||
"""扫码加好友"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "scan_add_friend", {})
|
||
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.get("/scan/my-qr", response_model=dict, tags=["扫一扫"])
|
||
async def show_my_qr(device_id: str, platform: Platform):
|
||
"""显示我的二维码"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "show_my_qr", {})
|
||
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("/scan/extract-qr", response_model=dict, tags=["扫一扫"])
|
||
async def extract_qr_from_image(device_id: str, platform: Platform):
|
||
"""从图片识别二维码"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "extract_qr_from_image", {})
|
||
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.get("/payment/code", response_model=dict, tags=["支付"])
|
||
async def show_payment_code(device_id: str, platform: Platform):
|
||
"""显示付款码"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "show_payment_code", {})
|
||
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("/payment/receive", response_model=dict, tags=["支付"])
|
||
async def receive_payment(device_id: str, platform: Platform, amount: str = "", desc: str = ""):
|
||
"""收款"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "receive_payment", {"amount": amount, "desc": desc})
|
||
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.get("/payment/wallet", response_model=dict, tags=["支付"])
|
||
async def view_wallet(device_id: str, platform: Platform):
|
||
"""查看钱包"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "view_wallet", {})
|
||
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.get("/payment/transactions", response_model=dict, tags=["支付"])
|
||
async def view_transactions(device_id: str, platform: Platform, limit: int = 20):
|
||
"""查看账单"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "view_transactions", {"limit": limit})
|
||
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("/payment/receive-red-packet", response_model=dict, tags=["支付"])
|
||
async def receive_red_packet(device_id: str, platform: Platform, from_id: str = ""):
|
||
"""领取红包"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "receive_red_packet", {"from_id": from_id})
|
||
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("/call/voice", response_model=dict, tags=["通话"])
|
||
async def voice_call(device_id: str, platform: Platform, user_id: str = ""):
|
||
"""语音通话"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "voice_call", {"user_id": user_id})
|
||
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("/call/video", response_model=dict, tags=["通话"])
|
||
async def video_call(device_id: str, platform: Platform, user_id: str = ""):
|
||
"""视频通话"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "video_call", {"user_id": user_id})
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十一、群发助手
|
||
# =============================================================================
|
||
|
||
class MassSendRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
content: str
|
||
user_ids: List[str]
|
||
|
||
@router.post("/mass-send", response_model=dict, tags=["群发助手"])
|
||
async def mass_send(req: MassSendRequest):
|
||
"""群发消息"""
|
||
_check_device_online(req.device_id)
|
||
guard = await _anti_ban_guard(req.device_id, req.platform.value, "batch_send", req.content)
|
||
if not guard["pass"]:
|
||
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "mass_send",
|
||
{"content": guard.get("content", req.content), "user_ids": req.user_ids}
|
||
)
|
||
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.get("/search/wechat", response_model=dict, tags=["搜一搜"])
|
||
async def wechat_search(device_id: str, platform: Platform, keyword: str = ""):
|
||
"""搜一搜"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "wechat_search", {"keyword": keyword})
|
||
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.get("/discover/top-stories", response_model=dict, tags=["看一看"])
|
||
async def top_stories(device_id: str, platform: Platform):
|
||
"""看一看"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "top_stories", {})
|
||
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.get("/wechat-sport/steps", response_model=dict, tags=["微信运动"])
|
||
async def get_steps(device_id: str, platform: Platform):
|
||
"""获取步数"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "get_steps", {})
|
||
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("/wechat-sport/like-steps", response_model=dict, tags=["微信运动"])
|
||
async def like_steps(device_id: str, platform: Platform, user_id: str = ""):
|
||
"""点赞步数"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "like_steps", {"user_id": user_id})
|
||
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("/location/send", response_model=dict, tags=["位置"])
|
||
async def send_location(device_id: str, platform: Platform, user_id: str = ""):
|
||
"""发送位置"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "send_location", {"user_id": user_id})
|
||
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("/location/share-realtime", response_model=dict, tags=["位置"])
|
||
async def share_real_time_location(device_id: str, platform: Platform, user_id: str = ""):
|
||
"""共享实时位置"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "share_real_time_location", {"user_id": user_id})
|
||
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("/emoji/send", response_model=dict, tags=["表情"])
|
||
async def send_emoji(device_id: str, platform: Platform, user_id: str = "", emoji_name: str = ""):
|
||
"""发送表情"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "send_emoji", {"user_id": user_id, "emoji_name": emoji_name})
|
||
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.get("/emoji/stickers", response_model=dict, tags=["表情"])
|
||
async def get_sticker_list(device_id: str, platform: Platform):
|
||
"""获取表情包列表"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "get_sticker_list", {})
|
||
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("/moments/set-cover", response_model=dict, tags=["朋友圈管理"])
|
||
async def set_moments_cover(device_id: str, platform: Platform):
|
||
"""设置朋友圈封面"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "set_moments_cover", {})
|
||
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("/moments/set-privacy", response_model=dict, tags=["朋友圈管理"])
|
||
async def set_moments_privacy(device_id: str, platform: Platform, days: int = 180):
|
||
"""设置朋友圈可见天数"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "set_moments_privacy", {"days": days})
|
||
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("/moments/share-link", response_model=dict, tags=["朋友圈管理"])
|
||
async def share_link_to_moments(device_id: str, platform: Platform, url: str = "", title: str = ""):
|
||
"""分享链接到朋友圈"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "forward_moments_link", {"url": url, "title": title})
|
||
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("/message/voice", response_model=dict, tags=["消息管理"])
|
||
async def send_voice_message(device_id: str, platform: Platform, user_id: str = "", duration: int = 3):
|
||
"""发送语音消息"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "send_voice_message", {"user_id": user_id, "duration": duration})
|
||
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("/file/send", response_model=dict, tags=["文件管理"])
|
||
async def send_file(device_id: str, platform: Platform, user_id: str = ""):
|
||
"""发送文件"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "send_file_from_chat", {"user_id": user_id})
|
||
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("/file/download", response_model=dict, tags=["文件管理"])
|
||
async def download_file(device_id: str, platform: Platform, user_id: str = ""):
|
||
"""下载文件"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "download_file", {"user_id": user_id})
|
||
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("/settings/do-not-disturb", response_model=dict, tags=["设置"])
|
||
async def toggle_dnd(device_id: str, platform: Platform, enable: bool = True):
|
||
"""勿扰模式"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "toggle_do_not_disturb", {"enable": enable})
|
||
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("/settings/clear-cache", response_model=dict, tags=["设置"])
|
||
async def clear_cache(device_id: str, platform: Platform):
|
||
"""清理缓存"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "clear_cache", {})
|
||
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("/settings/check-update", response_model=dict, tags=["设置"])
|
||
async def check_update(device_id: str, platform: Platform):
|
||
"""检查更新"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "check_for_update", {})
|
||
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("/settings/logout", response_model=dict, tags=["设置"])
|
||
async def logout(device_id: str, platform: Platform):
|
||
"""退出登录"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "logout", {})
|
||
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("/settings/switch-account", response_model=dict, tags=["设置"])
|
||
async def switch_account(device_id: str, platform: Platform):
|
||
"""切换账号"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "switch_account", {})
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "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 通道发送 — 优先走 Frida RPC,失败自动降级到 SDK/ADB 通道
|
||
|
||
路由策略:
|
||
1. 检查设备是否有活跃的 Frida Hook 会话
|
||
2. 有 → 通过 WebSocket 下发 hook_execute 指令(Agent 端 HookExecutor 执行)
|
||
3. 无 → 降级到 _send_via_sdk(ADB/u2 通道)
|
||
"""
|
||
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}")
|
||
|
||
mode = _get_device_mode(req.device_id)
|
||
|
||
if mode == "websocket":
|
||
try:
|
||
hook_params = {
|
||
"action": "send_message",
|
||
"params": {
|
||
"to_id": req.to_id,
|
||
"content": req.content,
|
||
"msg_type": req.msg_type.value if hasattr(req.msg_type, "value") else str(req.msg_type),
|
||
},
|
||
}
|
||
if script_id:
|
||
hook_params["script_id"] = script_id
|
||
|
||
timeout = req.timeout_seconds or settings.MESSAGE_SEND_TIMEOUT
|
||
result = await ws_hub.send_command(
|
||
req.device_id,
|
||
"hook_execute",
|
||
hook_params,
|
||
timeout=timeout,
|
||
)
|
||
if result and result.get("success"):
|
||
result["channel_used"] = "hook"
|
||
return result
|
||
logger.warning(f"[_send_via_hook] Frida RPC 失败,降级到 SDK: {result}")
|
||
except Exception as e:
|
||
logger.warning(f"[_send_via_hook] Hook 通道异常,降级到 SDK: {e}")
|
||
|
||
if mode == "adb":
|
||
device_serial = req.device_id
|
||
try:
|
||
from services.adb_device import adb_manager
|
||
hook_result = await asyncio.get_event_loop().run_in_executor(
|
||
None,
|
||
lambda: _try_local_frida_hook(device_serial, req),
|
||
)
|
||
if hook_result and hook_result.get("success"):
|
||
hook_result["channel_used"] = "hook"
|
||
return hook_result
|
||
logger.warning(f"[_send_via_hook] 本地 Frida 失败,降级到 ADB: {hook_result}")
|
||
except Exception as e:
|
||
logger.warning(f"[_send_via_hook] 本地 Frida 异常: {e}")
|
||
|
||
result = await _send_via_sdk(req)
|
||
if result.get("success"):
|
||
result["channel_used"] = "hook(degraded_to_sdk)"
|
||
return result
|
||
|
||
|
||
def _try_local_frida_hook(device_serial: str, req) -> dict:
|
||
"""尝试通过本地 Frida 直连设备执行 Hook(ADB 模式专用)"""
|
||
try:
|
||
import sys, os
|
||
agent_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "agent")
|
||
if agent_dir not in sys.path:
|
||
sys.path.insert(0, agent_dir)
|
||
|
||
from hook.frida_manager import FridaManager
|
||
from hook.hook_executor import HookExecutor
|
||
|
||
mgr = FridaManager(device_serial=device_serial)
|
||
if not mgr.start():
|
||
return {"success": False, "error": "Frida 连接失败"}
|
||
|
||
executor = HookExecutor(mgr)
|
||
result = executor.execute("send_message", {
|
||
"to_id": req.to_id,
|
||
"content": req.content,
|
||
"msg_type": req.msg_type.value if hasattr(req.msg_type, "value") else str(req.msg_type),
|
||
})
|
||
mgr.stop()
|
||
return result
|
||
except ImportError:
|
||
return {"success": False, "error": "frida 未安装"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
|
||
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": "设备离线"}
|
||
|
||
|
||
# ========== AF12: 风控中心监控 ==========
|
||
|
||
|
||
@router.get("/anti-ban/dashboard")
|
||
async def anti_ban_dashboard():
|
||
"""防封风控中心:汇总全局限流/账号生命周期/指纹碰撞状态"""
|
||
import time as _time
|
||
from services.rate_limiter import rate_limiter as _rl
|
||
|
||
devices = await device_manager.get_all_devices(limit=500)
|
||
|
||
total_devices = len(devices)
|
||
online = sum(1 for d in devices if d.get("status") == "online")
|
||
|
||
seen_fp: dict = {}
|
||
fp_collision_devices = []
|
||
for d in devices:
|
||
fp = d.get("fingerprint_hash", "")
|
||
if not fp:
|
||
continue
|
||
did = d.get("device_id", "")
|
||
seen_fp.setdefault(fp, []).append(did)
|
||
for fp, dids in seen_fp.items():
|
||
if len(dids) > 1:
|
||
for did in dids:
|
||
fp_collision_devices.append({
|
||
"device_id": did,
|
||
"collided_with": [x for x in dids if x != did],
|
||
})
|
||
|
||
lifecycle_summary = []
|
||
for d in devices[:50]:
|
||
did = d.get("device_id", "")
|
||
plat = d.get("platform", "wechat")
|
||
phase_info = await account_lifecycle.get_phase(did, plat)
|
||
rules = await account_lifecycle.get_rules(did, plat)
|
||
lifecycle_summary.append({
|
||
"device_id": did,
|
||
"phase": phase_info.value if hasattr(phase_info, "value") else str(phase_info),
|
||
"max_daily_add_friend": rules.get("max_daily_add_friend", "N/A"),
|
||
"max_daily_message": rules.get("max_daily_send_message", "N/A"),
|
||
})
|
||
|
||
return {
|
||
"timestamp": int(_time.time()),
|
||
"devices": {"total": total_devices, "online": online},
|
||
"fingerprint_collisions": fp_collision_devices,
|
||
"account_lifecycle": lifecycle_summary,
|
||
}
|
||
|
||
|
||
@router.get("/anti-ban/device/{device_id}")
|
||
async def anti_ban_device_detail(device_id: str):
|
||
"""单设备防封详情"""
|
||
device = await device_manager.get_device(device_id)
|
||
if not device:
|
||
raise HTTPException(status_code=404, detail="设备不存在")
|
||
|
||
plat = device.get("platform", "wechat")
|
||
phase = await account_lifecycle.get_phase(device_id, plat)
|
||
rules = await account_lifecycle.get_rules(device_id, plat)
|
||
fp = device.get("fingerprint_hash", "")
|
||
|
||
collision_result = {"collision": False, "collided_with": []}
|
||
if fp and device_manager.db:
|
||
collision_result = await device_manager.check_fingerprint_collision(
|
||
device_id, device.get("fingerprint_info", {})
|
||
)
|
||
|
||
return {
|
||
"device_id": device_id,
|
||
"status": device.get("status", "unknown"),
|
||
"fingerprint_hash": fp,
|
||
"fingerprint_collision": collision_result,
|
||
"account_phase": phase.value if hasattr(phase, "value") else str(phase),
|
||
"phase_rules": rules,
|
||
"last_heartbeat": str(device.get("last_heartbeat", "")),
|
||
}
|