Phantom 配置优先走 remote attach,解决 stock frida-server 被微信检测导致 RPC 映射层 send_message/get_contacts 间歇失败;oneclick 步骤3 默认部署随机名/端口。 Co-authored-by: Cursor <cursoragent@cursor.com>
2942 lines
114 KiB
Python
2942 lines
114 KiB
Python
"""
|
||
工作手机SDK v3.0 - 统一接口路由
|
||
存客宝重点对接的接口
|
||
|
||
功能模块:
|
||
1. 消息管理 - 发送/获取消息
|
||
2. 好友管理 - 添加/通过好友请求
|
||
3. 群聊管理 - 创建群/邀请入群/群发消息
|
||
4. 标签管理 - 添加/删除/查询标签
|
||
5. 朋友圈管理 - 发布/点赞/评论
|
||
6. 联系人管理 - 获取联系人列表
|
||
"""
|
||
|
||
from fastapi import APIRouter, HTTPException, Body
|
||
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. 微信后端强制 Frida Hook 主控(矩阵 v8.0.56 真机验收口径)
|
||
if platform == Platform.WECHAT and getattr(settings, "WECHAT_BACKEND_ONLY", True):
|
||
if device_online:
|
||
return Channel.HOOK
|
||
|
||
# 3. 设备在线用SDK控制
|
||
if device_online:
|
||
return Channel.SDK_CONTROL
|
||
|
||
# 4. 兜底用AI Agent
|
||
return Channel.AI_AGENT
|
||
|
||
|
||
# ========== 辅助函数 ==========
|
||
|
||
def _check_device_online(device_id: str):
|
||
"""检查设备是否在线(按优先级:Hook > WebSocket > ADB)。ADB 兜底保证只要 USB 连着就算在线。"""
|
||
if ws_hub.is_online(device_id):
|
||
return "agent"
|
||
adb_dev = adb_manager.get_device(device_id)
|
||
if adb_dev and adb_dev.is_online():
|
||
return "adb"
|
||
from services.connection_priority import connection_priority
|
||
best = connection_priority.get_best_mode(device_id)
|
||
if best:
|
||
return best.to_dict()["id"]
|
||
raise HTTPException(status_code=503, detail=f"设备不在线: {device_id}")
|
||
|
||
|
||
def _get_device_mode(device_id: str) -> str:
|
||
"""获取设备最优连接模式。ADB 兜底保证只要 USB 连着就能用。"""
|
||
if ws_hub.is_online(device_id):
|
||
return "websocket"
|
||
adb_dev = adb_manager.get_device(device_id)
|
||
if adb_dev and adb_dev.is_online():
|
||
return "adb"
|
||
from services.connection_priority import connection_priority
|
||
channel = connection_priority.choose_execution_channel(device_id)
|
||
if channel == "agent":
|
||
return "websocket"
|
||
if channel and channel != "offline":
|
||
return channel
|
||
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 _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 = 120,
|
||
hook_only: bool = False,
|
||
) -> dict:
|
||
"""执行技能并返回结果(Frida Hook 优先 / WebSocket / ADB),返回值附带 _channel_used。
|
||
|
||
通道优先级(与项目“主控 = Frida,辅助 = ADB”的设计一致):
|
||
1) hook_only=True 或本地能走 Frida → 本地 FridaManager + HookExecutor
|
||
2) WebSocket Agent 在线 → 下发 execute
|
||
3) ADB 在线 → ADB UI 自动化
|
||
4) 其他 → 设备离线
|
||
"""
|
||
mode = _get_device_mode(device_id)
|
||
# 微信后端强制策略:默认只允许 Frida Hook,不允许 WebSocket/u2 或 ADB UI 自动化回退。
|
||
# 用户要求“所有微信动作优先且固定在后端执行,不在前端显示操作”。
|
||
if platform == "wechat" and getattr(settings, "WECHAT_BACKEND_ONLY", True):
|
||
hook_only = True
|
||
|
||
# 〇、Frida Hook 主控分支(微信专用)
|
||
# 只要 ADB 在线且请求为 wechat,就会尝试本地 Frida;失败后按下面常规分支降级
|
||
if platform == "wechat" and mode in ("adb", "websocket"):
|
||
try:
|
||
adb_device = adb_manager.get_device(device_id)
|
||
if adb_device and adb_device.is_online():
|
||
serial = getattr(adb_device, "serial", device_id)
|
||
hook_result = await asyncio.get_running_loop().run_in_executor(
|
||
None,
|
||
lambda: _try_local_frida_action(serial, action, params),
|
||
)
|
||
if hook_result and hook_result.get("success"):
|
||
hook_result["_channel_used"] = "frida/hook"
|
||
return hook_result
|
||
if hook_only:
|
||
return {
|
||
"code": 503,
|
||
"success": False,
|
||
"message": (hook_result or {}).get("error") or "Frida 主控不可用",
|
||
"_channel_used": "frida/hook(failed)",
|
||
}
|
||
logger.warning(f"[hook] 本地 Frida 失败,降级到 {mode}: {hook_result}")
|
||
elif hook_only:
|
||
return {
|
||
"code": 503,
|
||
"success": False,
|
||
"message": "微信后端模式要求 ADB 在线 + Frida 可用;当前未检测到可用 ADB 设备",
|
||
"_channel_used": "none",
|
||
}
|
||
except Exception as e:
|
||
logger.warning(f"[hook] 本地 Frida 异常:{e}")
|
||
if hook_only:
|
||
return {"code": 503, "success": False, "message": str(e), "_channel_used": "frida/hook(failed)"}
|
||
|
||
if mode == "websocket":
|
||
result = await ws_hub.send_command(device_id, {
|
||
"type": "execute",
|
||
"data": {
|
||
"script": platform,
|
||
"action": action,
|
||
"params": params,
|
||
"hook_only": hook_only,
|
||
}
|
||
}, timeout=timeout)
|
||
agent_channel = result.get("channel", "u2") if isinstance(result, dict) else "u2"
|
||
result["_channel_used"] = f"websocket/{agent_channel}"
|
||
return result
|
||
|
||
elif mode == "adb":
|
||
if hook_only:
|
||
return {
|
||
"code": 503,
|
||
"message": "hook_only 需 Frida Hook 主控可用(已尝试本地 Frida 仍失败)",
|
||
"success": False,
|
||
"_channel_used": "none",
|
||
}
|
||
adb_device = adb_manager.get_device(device_id)
|
||
if not adb_device:
|
||
return {"success": False, "error": "ADB设备不可用", "_channel_used": "none"}
|
||
|
||
adb_result = await _execute_via_adb(adb_device, platform, action, params)
|
||
adb_result["_channel_used"] = "adb/sdk_control"
|
||
return adb_result
|
||
|
||
else:
|
||
return {"success": False, "error": "设备离线", "_channel_used": "offline"}
|
||
|
||
|
||
async def _execute_via_adb(device, platform: str, action: str, params: dict, timeout: int = 30) -> dict:
|
||
"""通过ADB执行技能操作 — 微信走专用引擎,其他平台走通用 UI 自动化。超时返回 error 而非无限挂起。"""
|
||
try:
|
||
if platform == "wechat":
|
||
from services.wechat_adb_engine import WeChatADBEngine
|
||
engine = WeChatADBEngine(device)
|
||
loop = asyncio.get_running_loop()
|
||
result = await asyncio.wait_for(
|
||
loop.run_in_executor(None, lambda: engine.execute(action, params)),
|
||
timeout=timeout
|
||
)
|
||
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 asyncio.TimeoutError:
|
||
logger.error(f"ADB执行超时: {platform}.{action} timeout={timeout}s")
|
||
return {"success": False, "error": f"操作超时({timeout}s)", "mode": "adb"}
|
||
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 = Field(..., description="操作名:send_message / get_contacts / get_profile / ... 共 107 个")
|
||
params: dict = Field(default_factory=dict, description="action 所需参数(键值对)")
|
||
hook_only: bool = Field(
|
||
default=False,
|
||
description="为 True 时仅走 Frida Hook,失败不降级 u2/ADB(纯 HWK 联调)",
|
||
)
|
||
|
||
|
||
HOOK_ALL_ACTIONS = {
|
||
"消息发送": ["send_message", "send_group_message"],
|
||
"消息获取": ["get_messages", "get_recent_messages", "search_messages"],
|
||
"联系人": ["get_contacts", "get_contact_info", "search_contacts"],
|
||
"好友管理": ["add_friend", "accept_friend", "delete_friend", "set_friend_remark", "get_friend_requests"],
|
||
"群管理": ["get_groups", "get_group_info", "get_group_members", "create_group", "invite_to_group", "remove_from_group", "set_group_announcement", "set_group_name", "quit_group"],
|
||
"朋友圈": ["post_moments", "get_moments", "like_moments", "comment_moments", "delete_moments"],
|
||
"账号管理": ["get_profile", "check_account_status", "set_nickname", "set_signature", "set_avatar", "set_sex", "set_region", "set_whats_up"],
|
||
"账号安全": ["unblock_self", "change_password", "bind_phone", "unbind_phone", "get_login_devices", "remove_login_device", "enable_fingerprint", "set_account_protection"],
|
||
"支付": ["send_red_packet", "receive_red_packet", "send_transfer", "receive_transfer", "get_wallet_balance", "get_transaction_history"],
|
||
"二维码": ["scan_qr_code", "generate_my_qr_code", "generate_group_qr_code", "add_friend_by_qr"],
|
||
"视频号": ["browse_channels", "like_channel_video", "comment_channel_video", "follow_channel", "unfollow_channel", "share_channel_video"],
|
||
"标签": ["get_labels", "create_label", "delete_label", "set_contact_label", "get_contacts_by_label"],
|
||
"收藏": ["get_favorites", "add_favorite", "delete_favorite"],
|
||
"设置": ["set_privacy", "set_notification", "clear_chat_history", "set_chat_background", "set_do_not_disturb", "pin_chat"],
|
||
"搜索": ["global_search"],
|
||
"小程序": ["open_mini_program", "get_recent_mini_programs", "share_mini_program"],
|
||
"文件传输": ["send_image", "send_video", "send_file", "send_voice", "send_location", "send_card", "send_link"],
|
||
"消息转发": ["forward_message", "forward_multiple", "revoke_message"],
|
||
"注册/登录": ["register_account", "login_by_password", "login_by_sms", "logout", "switch_account", "auto_register", "check_login_state", "get_sim_phone"],
|
||
"公众号": ["get_official_accounts", "follow_official_account", "unfollow_official_account", "get_official_account_articles"],
|
||
"表情": ["send_emoji", "add_custom_emoji"],
|
||
"浮窗": ["add_to_float", "remove_from_float"],
|
||
"设备信息": ["get_device_info", "get_storage_info", "get_network_info"],
|
||
"系统": ["get_hook_status", "get_process_info", "get_wechat_version", "batch_execute"],
|
||
}
|
||
|
||
|
||
@router.get("/hook/actions", response_model=dict, tags=["Hook模块管理"])
|
||
async def hook_actions():
|
||
"""获取 Hook 支持的全部操作清单 — 107 个操作 / 24 个模块"""
|
||
total = sum(len(v) for v in HOOK_ALL_ACTIONS.values())
|
||
return {
|
||
"code": 200,
|
||
"total_actions": total,
|
||
"total_modules": len(HOOK_ALL_ACTIONS),
|
||
"modules": HOOK_ALL_ACTIONS,
|
||
}
|
||
|
||
|
||
@router.get("/hook/probe/{device_id}", response_model=dict, tags=["Hook模块管理"])
|
||
async def hook_probe(device_id: str):
|
||
"""
|
||
探测设备 Frida Hook 能力 — 连接 → ping → 版本 → 更新 device_modules
|
||
|
||
用于首次入机或定期巡检,自动将 Hook 状态写入 device_modules.json。
|
||
"""
|
||
import asyncio
|
||
|
||
adb_dev = adb_manager.get_device(device_id)
|
||
serial = adb_dev.serial if adb_dev else device_id
|
||
|
||
result = await asyncio.get_running_loop().run_in_executor(
|
||
None, lambda: _probe_device_frida(serial),
|
||
)
|
||
|
||
from services.hook_module_service import hook_module_service
|
||
await hook_module_service.update_device_probe(device_id, {
|
||
"supports_hook": result.get("supports_hook", False),
|
||
"frida_version": result.get("frida_version", ""),
|
||
"root_status": result.get("root_status", False),
|
||
})
|
||
|
||
return {"code": 200, "device_id": device_id, **result}
|
||
|
||
|
||
@router.get("/hook/data/{device_id}", response_model=dict, tags=["Hook模块管理"])
|
||
async def hook_data(device_id: str, modules: str = "profile,contacts,groups,messages,labels"):
|
||
"""
|
||
一次性获取设备的 Hook 数据(联系人/消息/群/标签/资料等)
|
||
|
||
modules 参数用逗号分隔,可选值:
|
||
profile, contacts, groups, messages, labels, moments, accounts, hook_status, device_info
|
||
"""
|
||
import asyncio
|
||
|
||
adb_dev = adb_manager.get_device(device_id)
|
||
serial = adb_dev.serial if adb_dev else device_id
|
||
requested = [m.strip() for m in modules.split(",") if m.strip()]
|
||
|
||
result = await asyncio.get_running_loop().run_in_executor(
|
||
None, lambda: _fetch_hook_data(serial, requested),
|
||
)
|
||
return {"code": 200, "device_id": device_id, **result}
|
||
|
||
|
||
@router.post("/hook/execute", response_model=dict, tags=["Hook模块管理"])
|
||
async def hook_execute(req: HookExecuteRequest):
|
||
"""
|
||
Hawk Hook 统一执行 — 单接口控制整台手机
|
||
|
||
任意应用只需调此端点,传 action + params 即可执行 107 种操作。
|
||
通道优先级:Frida Hook → WebSocket Agent → ADB UI自动化。
|
||
|
||
示例:
|
||
```json
|
||
{"device_id":"dc9c23e00510","platform":"wechat","action":"send_message","params":{"to_id":"阿猫","content":"你好"}}
|
||
{"device_id":"dc9c23e00510","platform":"wechat","action":"get_profile","params":{},"hook_only":true}
|
||
{"device_id":"dc9c23e00510","platform":"wechat","action":"register_account","params":{"phone":"13800138000","nickname":"卡若AI"}}
|
||
```
|
||
`hook_only:true` 时仅 Frida RPC,失败不降级 u2(需 Agent 已连上且 Frida 附着微信)。
|
||
"""
|
||
_check_device_online(req.device_id)
|
||
result = await _execute_skill(
|
||
req.device_id,
|
||
req.platform.value,
|
||
req.action,
|
||
req.params or {},
|
||
hook_only=req.hook_only,
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
channel = result.get("_channel_used") or result.get("channel") or result.get("mode") or "sdk_control"
|
||
return {"code": 200, "data": payload, "action": req.action, "channel_used": channel}
|
||
|
||
|
||
# =============================================================================
|
||
# 一、消息管理接口
|
||
# =============================================================================
|
||
|
||
@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.model_dump(),
|
||
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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 二、好友管理接口
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 三、群聊管理接口
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 四、标签管理接口
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 五、朋友圈管理接口
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
return {
|
||
"code": 200,
|
||
"success": True,
|
||
"data": result.get("data", {}),
|
||
"channel_used": result.get("_channel_used", "sdk_control")
|
||
}
|
||
except Exception as e:
|
||
logger.exception(f"moments/post 异常: {e}")
|
||
return {
|
||
"code": 200,
|
||
"success": False,
|
||
"error": str(e),
|
||
"data": {},
|
||
"channel_used": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
@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": result.get("_channel_used", "sdk_control")
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 六、消息高级操作
|
||
# =============================================================================
|
||
|
||
class ForwardMessageRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform
|
||
to_id: str
|
||
content: str = ""
|
||
msg_svr_id: Optional[str] = None
|
||
|
||
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)
|
||
params = {"to_id": req.to_id, "content": req.content}
|
||
if req.msg_svr_id:
|
||
params["msg_svr_id"] = req.msg_svr_id
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "forward_message", params
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
class RecallMessageRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform = Platform.WECHAT
|
||
msg_svr_id: Optional[str] = None
|
||
|
||
@router.post("/message/recall", response_model=dict, tags=["消息管理"])
|
||
async def recall_message(req: RecallMessageRequest):
|
||
"""撤回最近一条消息"""
|
||
_check_device_online(req.device_id)
|
||
params = {}
|
||
if req.msg_svr_id:
|
||
params["msg_svr_id"] = req.msg_svr_id
|
||
result = await _execute_skill(req.device_id, req.platform.value, "recall_message", params)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 七、个人设置接口
|
||
# =============================================================================
|
||
|
||
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 = Platform.WECHAT):
|
||
"""获取当前微信账号资料"""
|
||
_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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 八、账号安全接口
|
||
# =============================================================================
|
||
|
||
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 = Platform.WECHAT):
|
||
"""检查账号状态"""
|
||
_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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 九、收藏管理接口
|
||
# =============================================================================
|
||
|
||
class AddFavoriteRequest(BaseModel):
|
||
device_id: str
|
||
platform: Platform = Platform.WECHAT
|
||
content_desc: Optional[str] = None
|
||
content: Optional[str] = None
|
||
content_type: Optional[str] = "text"
|
||
msg_svr_id: Optional[str] = None
|
||
|
||
@router.post("/favorites/add", response_model=dict, tags=["收藏管理"])
|
||
async def add_favorite(req: AddFavoriteRequest):
|
||
"""收藏消息"""
|
||
_check_device_online(req.device_id)
|
||
params = {"type": req.content_type or "text"}
|
||
if req.msg_svr_id:
|
||
params["msg_svr_id"] = req.msg_svr_id
|
||
if req.content:
|
||
params["content"] = req.content
|
||
if req.content_desc:
|
||
params["content_desc"] = req.content_desc
|
||
result = await _execute_skill(
|
||
req.device_id, req.platform.value, "add_favorite", params
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
@router.get("/favorites/list", response_model=dict, tags=["收藏管理"])
|
||
async def get_favorites(device_id: str, platform: Platform = Platform.WECHAT, 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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十、聊天设置接口
|
||
# =============================================================================
|
||
|
||
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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十一、联系人搜索接口
|
||
# =============================================================================
|
||
|
||
@router.get("/contacts/search", response_model=dict, tags=["好友管理"])
|
||
async def search_contacts(device_id: str, keyword: str, platform: Platform = Platform.WECHAT):
|
||
"""搜索联系人"""
|
||
_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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十二、群聊高级操作
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十三、朋友圈高级操作
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十四、支付接口
|
||
# =============================================================================
|
||
|
||
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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十五、小程序 & 公众号
|
||
# =============================================================================
|
||
|
||
@router.post("/miniprogram/open", response_model=dict, tags=["小程序"])
|
||
async def open_mini_program(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
name: str = Body(""),
|
||
app_id: str = Body(""),
|
||
):
|
||
"""打开小程序"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(
|
||
device_id, platform.value, "open_mini_program", {"name": name, "app_id": app_id or name}
|
||
)
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十六、解封与限制管理
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十七、视频号
|
||
# =============================================================================
|
||
|
||
@router.get("/video-channel/list", response_model=dict, tags=["视频号"])
|
||
async def get_video_list(device_id: str, platform: Platform = Platform.WECHAT, 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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十八、扫一扫
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 十九、支付增强
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十、通话
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十一、群发助手
|
||
# =============================================================================
|
||
|
||
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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十二、搜一搜/看一看
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十三、微信运动
|
||
# =============================================================================
|
||
|
||
@router.get("/wechat-sport/steps", response_model=dict, tags=["微信运动"])
|
||
async def get_steps(device_id: str, platform: Platform = Platform.WECHAT):
|
||
"""获取步数"""
|
||
_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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十四、位置分享
|
||
# =============================================================================
|
||
|
||
@router.post("/location/send", response_model=dict, tags=["位置"])
|
||
async def send_location(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
to_id: str = Body(""),
|
||
user_id: str = Body(""),
|
||
latitude: float = Body(0),
|
||
longitude: float = Body(0),
|
||
name: str = Body(""),
|
||
label: str = Body(""),
|
||
):
|
||
"""发送位置"""
|
||
_check_device_online(device_id)
|
||
tid = to_id or user_id
|
||
result = await _execute_skill(device_id, platform.value, "send_location", {
|
||
"to_id": tid, "latitude": latitude, "longitude": longitude,
|
||
"label": label or name, "name": name,
|
||
})
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十五、表情管理
|
||
# =============================================================================
|
||
|
||
@router.post("/emoji/send", response_model=dict, tags=["表情"])
|
||
async def send_emoji(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
to_id: str = Body(""),
|
||
user_id: str = Body(""),
|
||
emoji_name: str = Body(""),
|
||
emoji_md5: str = Body(""),
|
||
):
|
||
"""发送表情"""
|
||
_check_device_online(device_id)
|
||
tid = to_id or user_id
|
||
result = await _execute_skill(device_id, platform.value, "send_emoji", {
|
||
"to_id": tid, "user_id": tid, "emoji_name": emoji_name, "emoji_md5": emoji_md5 or emoji_name,
|
||
})
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
@router.get("/emoji/stickers", response_model=dict, tags=["表情"])
|
||
async def get_sticker_list(device_id: str, platform: Platform = Platform.WECHAT):
|
||
"""获取表情包列表"""
|
||
_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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十六、朋友圈增强
|
||
# =============================================================================
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@router.post("/moments/set-privacy", response_model=dict, tags=["朋友圈管理"])
|
||
async def set_moments_privacy(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
privacy_type: str = Body("all"),
|
||
days: int = Body(180),
|
||
):
|
||
"""设置朋友圈可见天数"""
|
||
_check_device_online(device_id)
|
||
result = await _execute_skill(device_id, platform.value, "set_moments_privacy", {
|
||
"privacy_type": privacy_type,
|
||
"days": days,
|
||
})
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十七、语音消息
|
||
# =============================================================================
|
||
|
||
@router.post("/message/voice", response_model=dict, tags=["消息管理"])
|
||
async def send_voice_message(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
to_id: str = Body(""),
|
||
user_id: str = Body(""),
|
||
duration: int = Body(3),
|
||
voice_path: str = Body(""),
|
||
):
|
||
"""发送语音消息"""
|
||
_check_device_online(device_id)
|
||
tid = to_id or user_id
|
||
result = await _execute_skill(device_id, platform.value, "send_voice_message", {
|
||
"to_id": tid, "user_id": tid, "duration": duration, "voice_path": voice_path,
|
||
})
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十八、文件管理
|
||
# =============================================================================
|
||
|
||
@router.post("/file/send", response_model=dict, tags=["文件管理"])
|
||
async def send_file(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
to_id: str = Body(""),
|
||
user_id: str = Body(""),
|
||
file_path: str = Body(""),
|
||
):
|
||
"""发送文件"""
|
||
_check_device_online(device_id)
|
||
tid = to_id or user_id
|
||
result = await _execute_skill(device_id, platform.value, "send_file", {
|
||
"to_id": tid, "file_path": file_path,
|
||
})
|
||
payload = result.get("data") if result.get("data") is not None else result
|
||
return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 二十九、设置管理
|
||
# =============================================================================
|
||
|
||
@router.post("/settings/do-not-disturb", response_model=dict, tags=["设置"])
|
||
async def toggle_dnd(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
enable: bool = Body(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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@router.post("/settings/clear-cache", response_model=dict, tags=["设置"])
|
||
async def clear_cache(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
):
|
||
"""清理缓存"""
|
||
_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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@router.post("/settings/check-update", response_model=dict, tags=["设置"])
|
||
async def check_update(
|
||
device_id: str = Body(...),
|
||
platform: Platform = Body(Platform.WECHAT),
|
||
):
|
||
"""检查更新"""
|
||
_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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
@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": result.get("_channel_used", "sdk_control")}
|
||
|
||
|
||
# =============================================================================
|
||
# 内部实现方法
|
||
# =============================================================================
|
||
|
||
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 in ("adb", "websocket"):
|
||
adb_device = adb_manager.get_device(req.device_id)
|
||
device_serial = getattr(adb_device, "serial", req.device_id) if adb_device else req.device_id
|
||
try:
|
||
hook_result = await asyncio.get_running_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 失败: {hook_result}")
|
||
if getattr(settings, "WECHAT_BACKEND_ONLY", True):
|
||
err = (hook_result or {}).get("error") or "Frida Hook 发送失败"
|
||
return {"success": False, "error": err, "channel_used": "frida/hook(failed)"}
|
||
except Exception as e:
|
||
logger.warning(f"[_send_via_hook] 本地 Frida 异常: {e}")
|
||
if getattr(settings, "WECHAT_BACKEND_ONLY", True):
|
||
return {"success": False, "error": str(e), "channel_used": "frida/hook(failed)"}
|
||
|
||
if getattr(settings, "WECHAT_BACKEND_ONLY", True):
|
||
return {"success": False, "error": "微信后端模式要求 Frida Hook 可用", "channel_used": "none"}
|
||
|
||
result = await _send_via_sdk(req)
|
||
if result.get("success"):
|
||
result["channel_used"] = "hook(degraded_to_sdk)"
|
||
return result
|
||
|
||
|
||
# Frida 会话池:避免每条 API 反复 attach/detach 导致连跑验收失败
|
||
_frida_session_pool: dict = {}
|
||
|
||
|
||
def _load_phantom_frida_config() -> dict:
|
||
"""读取 anti_detect/phantom_frida_config.json(随机端口反检测 frida-server)"""
|
||
import json
|
||
import os
|
||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
for rel in (
|
||
os.path.join("scripts", "anti_detect", "phantom_frida_config.json"),
|
||
os.path.join("sdk", "scripts", "anti_detect", "phantom_frida_config.json"),
|
||
):
|
||
path = os.path.join(base_dir, rel)
|
||
if os.path.isfile(path):
|
||
try:
|
||
with open(path, encoding="utf-8") as f:
|
||
cfg = json.load(f)
|
||
if cfg.get("listen_port"):
|
||
return cfg
|
||
except Exception:
|
||
pass
|
||
return {}
|
||
|
||
|
||
def _get_frida_manager(device_serial: str):
|
||
"""获取 FridaManager:优先 phantom/remote 随机端口,否则 usb"""
|
||
import sys, os, subprocess
|
||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
candidate_dirs = [
|
||
os.path.join(base_dir, "agent"),
|
||
os.path.join(base_dir, "sdk", "agent"),
|
||
]
|
||
for agent_dir in candidate_dirs:
|
||
if os.path.isdir(agent_dir) and agent_dir not in sys.path:
|
||
sys.path.insert(0, agent_dir)
|
||
from hook.frida_manager import FridaManager
|
||
|
||
phantom = _load_phantom_frida_config()
|
||
if phantom.get("listen_port"):
|
||
port = int(phantom["listen_port"])
|
||
adb_serial = phantom.get("device_serial") or device_serial
|
||
try:
|
||
subprocess.run(
|
||
["adb", "-s", adb_serial, "forward", f"tcp:{port}", f"tcp:{port}"],
|
||
capture_output=True, timeout=5, check=False,
|
||
)
|
||
except Exception:
|
||
pass
|
||
return FridaManager(
|
||
device_serial=adb_serial,
|
||
mode="remote",
|
||
gadget_host="127.0.0.1",
|
||
gadget_port=port,
|
||
)
|
||
|
||
return FridaManager(device_serial=device_serial, mode="usb")
|
||
|
||
|
||
def _probe_device_frida(device_serial: str) -> dict:
|
||
"""探测设备 Frida Hook 能力:连接 → ping → 版本 → profile"""
|
||
import subprocess
|
||
probe = {
|
||
"supports_hook": False,
|
||
"frida_version": "",
|
||
"root_status": False,
|
||
"wechat_version": "",
|
||
"profile": {},
|
||
"hook_tests": {},
|
||
}
|
||
|
||
try:
|
||
r = subprocess.run(
|
||
["adb", "-s", device_serial, "shell", "su", "-c", "id"],
|
||
capture_output=True, text=True, timeout=5,
|
||
)
|
||
probe["root_status"] = "uid=0" in (r.stdout or "")
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
mgr = _get_frida_manager(device_serial)
|
||
if not mgr.start():
|
||
probe["hook_tests"]["connect"] = "failed"
|
||
return probe
|
||
|
||
probe["hook_tests"]["connect"] = "ok"
|
||
rpc = mgr.rpc
|
||
|
||
try:
|
||
pong = rpc.ping()
|
||
probe["hook_tests"]["ping"] = str(pong)[:200]
|
||
probe["supports_hook"] = True
|
||
except Exception as e:
|
||
probe["hook_tests"]["ping"] = str(e)[:200]
|
||
|
||
try:
|
||
ver = rpc.get_wechat_version()
|
||
probe["wechat_version"] = str(ver)
|
||
probe["hook_tests"]["wechat_version"] = str(ver)
|
||
except Exception as e:
|
||
probe["hook_tests"]["wechat_version"] = str(e)[:200]
|
||
|
||
try:
|
||
profile = rpc.get_profile({})
|
||
if isinstance(profile, dict):
|
||
probe["profile"] = profile.get("data", profile)
|
||
probe["hook_tests"]["profile"] = "ok"
|
||
except Exception as e:
|
||
probe["hook_tests"]["profile"] = str(e)[:200]
|
||
|
||
try:
|
||
import frida
|
||
probe["frida_version"] = frida.__version__
|
||
except Exception:
|
||
pass
|
||
|
||
mgr.stop()
|
||
except ImportError:
|
||
probe["hook_tests"]["connect"] = "frida not installed"
|
||
except Exception as e:
|
||
probe["hook_tests"]["connect"] = str(e)[:200]
|
||
|
||
return probe
|
||
|
||
|
||
def _fetch_hook_data(device_serial: str, modules: list) -> dict:
|
||
"""通过本地 Frida 批量获取 Hook 数据"""
|
||
import re
|
||
def to_snake(name):
|
||
return re.sub(r'(?<=[a-z0-9])([A-Z])', r'_\1', name).lower()
|
||
|
||
MODULE_MAP = {
|
||
"profile": ("getProfile", {}),
|
||
"contacts": ("getContacts", {"limit": 200}),
|
||
"groups": ("getGroups", {}),
|
||
"messages": ("getMessages", {"limit": 20}),
|
||
"labels": ("getLabels", {}),
|
||
"moments": ("getMoments", {"limit": 10}),
|
||
"accounts": ("getOfficialAccounts", {}),
|
||
"hook_status": ("getHookStatus", {}),
|
||
"device_info": ("getDeviceInfo", None),
|
||
"wechat_version": ("getWechatVersion", None),
|
||
"process_info": ("getProcessInfo", {}),
|
||
"storage_info": ("getStorageInfo", {}),
|
||
"network_info": ("getNetworkInfo", {}),
|
||
"login_state": ("checkLoginState", {}),
|
||
"favorites": ("getFavorites", {"limit": 20}),
|
||
}
|
||
|
||
data = {}
|
||
try:
|
||
mgr = _get_frida_manager(device_serial)
|
||
if not mgr.start():
|
||
return {"success": False, "error": "Frida 连接失败", "data": {}}
|
||
|
||
rpc = mgr.rpc
|
||
for mod in modules:
|
||
if mod not in MODULE_MAP:
|
||
data[mod] = {"error": f"未知模块: {mod}"}
|
||
continue
|
||
camel_name, params = MODULE_MAP[mod]
|
||
snake_name = to_snake(camel_name)
|
||
try:
|
||
fn = getattr(rpc, snake_name, None)
|
||
if fn is None:
|
||
data[mod] = {"error": f"RPC 方法不存在: {snake_name}"}
|
||
continue
|
||
resp = fn(params) if params is not None else fn()
|
||
data[mod] = resp if isinstance(resp, dict) else {"data": resp}
|
||
except Exception as e:
|
||
data[mod] = {"error": str(e)[:200]}
|
||
|
||
mgr.stop()
|
||
return {"success": True, "data": data, "modules_fetched": list(data.keys())}
|
||
except ImportError:
|
||
return {"success": False, "error": "frida 未安装", "data": {}}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)[:200], "data": data}
|
||
|
||
|
||
def _get_frida_session(device_serial: str):
|
||
"""获取或复用已连接的 FridaManager(矩阵/E2E 连跑稳定性)"""
|
||
import time
|
||
mgr = _frida_session_pool.get(device_serial)
|
||
if mgr is not None and getattr(mgr, "connected", False):
|
||
return mgr
|
||
for attempt in range(3):
|
||
mgr = _get_frida_manager(device_serial)
|
||
if mgr.start():
|
||
_frida_session_pool[device_serial] = mgr
|
||
return mgr
|
||
time.sleep(0.8 * (attempt + 1))
|
||
_frida_session_pool.pop(device_serial, None)
|
||
return None
|
||
|
||
|
||
def _try_local_frida_action(device_serial: str, action: str, params: dict) -> dict:
|
||
"""任意 action 走本地 Frida + HookExecutor。action 名与 hook_executor.ACTION_TO_RPC 一致。"""
|
||
try:
|
||
mgr = _get_frida_session(device_serial)
|
||
if not mgr:
|
||
return {"success": False, "error": "Frida 连接失败"}
|
||
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.hook_executor import HookExecutor
|
||
executor = HookExecutor(mgr)
|
||
norm = dict(params or {})
|
||
if action == "send_message" and "msg_type" not in norm:
|
||
norm["msg_type"] = "text"
|
||
result = executor.execute(action, norm)
|
||
return result
|
||
except ImportError:
|
||
return {"success": False, "error": "frida 未安装"}
|
||
except Exception as e:
|
||
_frida_session_pool.pop(device_serial, None)
|
||
return {"success": False, "error": str(e)}
|
||
|
||
|
||
def _try_local_frida_hook(device_serial: str, req) -> dict:
|
||
"""尝试通过本地 Frida 直连设备执行 Hook(ADB 模式专用)"""
|
||
try:
|
||
mgr = _get_frida_session(device_serial)
|
||
if not mgr:
|
||
return {"success": False, "error": "Frida 连接失败"}
|
||
|
||
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.hook_executor import HookExecutor
|
||
|
||
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),
|
||
})
|
||
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": "设备离线"}
|
||
|
||
|
||
# =============================================================================
|
||
# 三十、自动注册 & 设备初始化
|
||
# =============================================================================
|
||
|
||
class AutoRegisterRequest(BaseModel):
|
||
"""自动注册请求"""
|
||
device_id: str
|
||
nickname: str = "卡若AI"
|
||
password: str = ""
|
||
test_msg_to: str = ""
|
||
test_msg_content: str = "你好,我是卡若AI工作手机"
|
||
|
||
|
||
class CheckLoginStateRequest(BaseModel):
|
||
"""检查登录状态请求"""
|
||
device_id: str
|
||
|
||
|
||
class GetSimPhoneRequest(BaseModel):
|
||
"""获取 SIM 卡手机号请求"""
|
||
device_id: str
|
||
|
||
|
||
@router.post("/auto-register/full", response_model=dict, tags=["自动注册"])
|
||
async def auto_register_full(req: AutoRegisterRequest):
|
||
"""
|
||
全自动微信注册 — 一键完成
|
||
|
||
流程:
|
||
1. 检测微信登录状态
|
||
2. 未登录 → 自动从 SIM 获取手机号 → 注册
|
||
3. 自动读取短信验证码 → 填写
|
||
4. 设置昵称/密码 → 完成注册
|
||
5. 可选:发送测试消息
|
||
"""
|
||
from services.auto_register import run_auto_register
|
||
result = await run_auto_register(
|
||
device_id=req.device_id,
|
||
nickname=req.nickname,
|
||
password=req.password,
|
||
test_msg_to=req.test_msg_to,
|
||
test_msg_content=req.test_msg_content,
|
||
)
|
||
return {"code": 200, "data": result}
|
||
|
||
|
||
@router.post("/auto-register/check-state", response_model=dict, tags=["自动注册"])
|
||
async def check_wechat_login_state(req: CheckLoginStateRequest):
|
||
"""检查微信登录状态(已登录/登录页/注册页/未安装)"""
|
||
from services.auto_register import WeChatAutoRegister
|
||
from services.adb_device import adb_manager
|
||
|
||
adb_device = adb_manager.get_device(req.device_id)
|
||
if not adb_device:
|
||
return {"code": 503, "data": {"error": f"设备不可用: {req.device_id}"}}
|
||
|
||
engine = WeChatAutoRegister(adb_device)
|
||
loop = asyncio.get_running_loop()
|
||
state = await loop.run_in_executor(None, engine.check_wechat_login_state)
|
||
return {"code": 200, "data": state}
|
||
|
||
|
||
@router.post("/auto-register/get-sim-phone", response_model=dict, tags=["自动注册"])
|
||
async def get_sim_phone(req: GetSimPhoneRequest):
|
||
"""从设备 SIM 卡获取手机号"""
|
||
from services.auto_register import WeChatAutoRegister
|
||
from services.adb_device import adb_manager
|
||
|
||
adb_device = adb_manager.get_device(req.device_id)
|
||
if not adb_device:
|
||
return {"code": 503, "data": {"error": f"设备不可用: {req.device_id}"}}
|
||
|
||
engine = WeChatAutoRegister(adb_device)
|
||
loop = asyncio.get_running_loop()
|
||
sim_info = await loop.run_in_executor(None, engine.get_sim_phone_number)
|
||
return {"code": 200, "data": sim_info}
|
||
|
||
|
||
# ========== AF12: 风控中心监控 ==========
|
||
|
||
|
||
@router.get("/anti-ban/dashboard")
|
||
async def anti_ban_dashboard():
|
||
"""防封风控中心:汇总全局限流/账号生命周期/指纹碰撞状态"""
|
||
import time as _time
|
||
from services.rate_limiter import rate_limiter as _rl
|
||
|
||
try:
|
||
devices = await device_manager.get_all_devices(limit=500)
|
||
except Exception:
|
||
devices = []
|
||
|
||
online_ws = {d["device_id"]: d for d in ws_hub.get_online_devices()}
|
||
from services.adb_device import adb_manager as _adb
|
||
adb_serials = _adb.scan_devices()
|
||
seen = {d.get("device_id") for d in devices}
|
||
for did, info in online_ws.items():
|
||
if did not in seen:
|
||
devices.append({**info, "status": "online"})
|
||
seen.add(did)
|
||
for serial in adb_serials:
|
||
if serial not in seen:
|
||
devices.append({"device_id": serial, "status": "adb"})
|
||
seen.add(serial)
|
||
|
||
total_devices = len(devices)
|
||
online = sum(1 for d in devices if d.get("status") in ("online", "adb"))
|
||
|
||
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")
|
||
try:
|
||
phase_info = await account_lifecycle.get_phase(did, plat)
|
||
rules = await account_lifecycle.get_rules(did, plat)
|
||
except Exception:
|
||
phase_info = "unknown"
|
||
rules = {}
|
||
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", "")),
|
||
}
|