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

3768 lines
150 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
工作手机SDK v3.0 - 统一接口路由
存客宝重点对接的接口
功能模块:
1. 消息管理 - 发送/获取消息
2. 好友管理 - 添加/通过好友请求
3. 群聊管理 - 创建群/邀请入群/群发消息
4. 标签管理 - 添加/删除/查询标签
5. 朋友圈管理 - 发布/点赞/评论
6. 联系人管理 - 获取联系人列表
"""
from fastapi import APIRouter, HTTPException, Body
from typing import Optional, List
from pydantic import BaseModel, Field
from enum import Enum
import asyncio
import logging
import re
import time
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
from services.device_id_util import device_id_md5
router = APIRouter()
logger = logging.getLogger(__name__)
DEFAULT_CONTACT_PULL_LIMIT = 10000
MAX_CONTACT_PULL_LIMIT = 20000
DEFAULT_MESSAGE_PULL_LIMIT = 3000
MAX_MESSAGE_PULL_LIMIT = 5000
# ========== 枚举定义 ==========
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 = DEFAULT_MESSAGE_PULL_LIMIT
offset: int = 0
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 MessageSyncSinceRequest(BaseModel):
"""增量同步消息请求"""
device_id: str
platform: Platform = Platform.WECHAT
conversation_id: Optional[str] = None
since_time: int = 0
limit: int = DEFAULT_MESSAGE_PULL_LIMIT
offset: int = 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, platform: str = "wechat", action: str = ""):
"""检查设备是否在线(经 device_transport 统一策略,无线主控 WS 优先)。"""
from services.device_transport import device_transport
return device_transport.check_device_online(device_id, platform, action)
def _get_device_mode(device_id: str, platform: str = "wechat", action: str = "") -> str:
"""获取设备传输模式(经 device_transport 封装,禁止业务层自行判 ADB"""
from services.device_transport import device_transport
return device_transport.resolve_mode(device_id, platform, action)
def _payload_of(result: dict) -> dict:
"""兼容 {data:{...}} 与直接 payload 返回。"""
if not isinstance(result, dict):
return {}
payload = result.get("data") if isinstance(result.get("data"), dict) else result
return payload if isinstance(payload, dict) else {}
def _list_from_payload(payload: dict, *keys: str) -> list:
"""从不同 Hook/Agent 返回格式里抽取列表。"""
if not isinstance(payload, dict):
return []
for key in keys:
value = payload.get(key)
if isinstance(value, list):
return value
if isinstance(value, dict):
for nested in ("items", "list", "data"):
nested_value = value.get(nested)
if isinstance(nested_value, list):
return nested_value
data = payload.get("data")
if isinstance(data, dict):
return _list_from_payload(data, *keys)
return []
def _message_timestamp(message: dict) -> int:
"""把常见消息时间字段归一为秒级 int无法识别时返回 0。"""
if not isinstance(message, dict):
return 0
for key in ("timestamp", "create_time", "createTime", "time", "msg_time", "msgTime"):
raw = message.get(key)
if raw is None:
continue
try:
ts = int(float(raw))
return ts // 1000 if ts > 10_000_000_000 else ts
except (TypeError, ValueError):
continue
return 0
def _matches_contact(contact: dict, keyword: str) -> bool:
if not keyword:
return True
text = " ".join(str(contact.get(k, "")) for k in (
"nickname", "display_name", "remark", "alias", "wechat_id", "wxid", "user_id", "username"
))
return keyword.lower() in text.lower()
def _bounded_limit(value, default: int, maximum: int) -> int:
try:
num = int(value)
except (TypeError, ValueError):
num = default
return min(max(num, 1), maximum)
def _bounded_offset(value) -> int:
try:
num = int(value)
except (TypeError, ValueError):
num = 0
return max(num, 0)
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动作 → 敏感词 → 内容差异化
"""
import os
if os.environ.get("SDK_MATRIX_VERIFY") == "1":
return {"pass": True, "reason": "", "content": content}
from services.anti_ban_alert import (
alert_daily_limit, alert_outside_hours, alert_sensitive_content,
)
from services.rate_limiter import (
OutsideOperationHours, LongPauseRequired, HighRiskComboBlocked,
SilentThrottleCooldown, classify_risk,
)
def _blocked(reason: str) -> dict:
# AB-06附带风险分级 risk_actionblock/needs_human/log
return {"pass": False, "reason": reason, "content": content,
"risk_action": classify_risk(reason)}
try:
lifecycle_check = await account_lifecycle.check_allowed(device_id, platform, action)
if not lifecycle_check["allowed"]:
return _blocked(lifecycle_check["reason"])
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 _blocked(str(e))
except DailyLimitExceeded as e:
asyncio.ensure_future(alert_daily_limit(device_id, platform, action, -1, -1))
return _blocked(str(e))
except SilentThrottleCooldown as e:
# AB-02静默限流熔断期写类动作 fast-failrisk=needs_human
return _blocked(str(e))
except (LongPauseRequired, HighRiskComboBlocked) as e:
# AB-03长会话中断 / 高危组合互斥fast-fail 不长 sleep
return _blocked(str(e))
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}
# 必须走 u2 UI 自动化的微信动作 — 真源services/device_transport.py
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) 其他 → 设备离线
"""
from services.device_transport import device_transport
mode = _get_device_mode(device_id, platform, action)
ws_hook_only = device_transport.ws_hook_only(platform)
hook_only = device_transport.should_force_hook_only(platform, action, hook_only)
# -A、WebSocket + Frida 主控(经 device_transport 统一封装,禁止绕过 WS 直连 ADB
if ws_hook_only and mode != "adb":
if not device_transport.is_ws_online(device_id):
return device_transport.offline_payload(device_id)
mode = "websocket"
# -B、Frida Hook 本地 ADB attach仅 WECHAT_WS_HOOK_ONLY=false 时)
if platform == "wechat" and mode == "adb" and not ws_hook_only:
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":
return await device_transport.execute_via_ws(
device_id, platform, action, params, timeout=timeout, hook_only=hook_only,
)
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()
adb_timeout = 90 if action == "send_message" else timeout
result = await asyncio.wait_for(
loop.run_in_executor(None, lambda: engine.execute(action, params)),
timeout=adb_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("/antiban/status", response_model=dict, tags=["防封风控"])
async def antiban_status(device_id: Optional[str] = None):
"""AB-05 防封看板数据源:聚合限流配置 + 操作时段 + 设备 guard 红灯 / RiskSentinel。
- 无 device_id返回全局限流策略与当前操作时段。
- 带 device_id附加该设备 register/心跳上报的 anti_bandevice_guard.redlight、RiskSentinel
"""
from services.rate_limiter import PLATFORM_LIMITS, OPERATION_HOURS
data = {
"operation_hours": {
"range": list(OPERATION_HOURS),
"in_window": rate_limiter.check_operation_hours(),
"tz": "Asia/Shanghai",
},
"platform_limits": PLATFORM_LIMITS,
"long_pause": {"every_actions": "8-12", "pause_seconds": "600-1800"},
"high_risk_exclusive": [
"add_friend", "batch_add_friend", "batch_send",
"mass_send", "create_group", "invite_to_group",
],
}
if device_id:
info = ws_hub.get_device_info(device_id) or {}
anti = info.get("anti_ban") or {}
guard = anti.get("guard") or {}
data["device"] = {
"device_id": device_id,
"online": ws_hub.is_online(device_id),
"redlight": guard.get("redlight"),
"guard_warnings": guard.get("warnings"),
"sentinel": anti.get("sentinel"),
"nurture": anti.get("nurture"),
# AB-02/03 运行态:熔断 / 长暂停 / 动作节拍 / 最近高危动作
"runtime": rate_limiter.get_runtime_state(device_id),
}
return {"code": 200, "data": data}
@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
无线主控WORKPHONE_WS_FIRSTAgent WebSocket 在线时经 WS 探测,不依赖主机 ADB。
"""
import asyncio
ws_first = getattr(settings, "WORKPHONE_WS_FIRST", True)
ws_hook_only = getattr(settings, "WECHAT_WS_HOOK_ONLY", True)
host_adb_probe = getattr(settings, "WORKPHONE_HOST_ADB_PROBE", False)
if (ws_first or ws_hook_only) and ws_hub.is_online(device_id):
result = await _probe_device_via_ws(device_id)
elif host_adb_probe:
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),
)
else:
result = {
"supports_hook": False,
"frida_version": "",
"root_status": False,
"wechat_version": "",
"profile": {},
"hook_tests": {"connect": "websocket-only: agent offline or frida not ready"},
"transport": "websocket",
"probe_detail": "无线主控:需设备端 Termux Agent + 本机 Frida无需主机 ADB",
}
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, "device_id_md5": device_id_md5(device_id), **result}
async def _probe_device_via_ws(device_id: str) -> dict:
"""经 WebSocket Agent 探测 Frida无线主控无需主机 ADB"""
info = ws_hub.get_device_info(device_id) or {}
capabilities = set(info.get("capabilities") or [])
reported_ready = bool(info.get("frida_available")) or bool(
{"frida", "hook", "frida_rpc"} & capabilities
)
probe = {
"supports_hook": reported_ready,
"frida_version": "",
"root_status": bool(info.get("root_status")),
"wechat_version": "",
"profile": {},
"hook_tests": {},
"transport": "websocket",
"frida_available_reported": reported_ready,
}
ping = await _execute_skill(device_id, "wechat", "ping", {}, timeout=45, hook_only=True)
probe["hook_tests"]["ping"] = str(ping)[:300]
pdata = ping.get("data") if isinstance(ping.get("data"), dict) else {}
frida_ok = (
reported_ready
or bool(pdata.get("frida_connected"))
or bool(ping.get("success"))
or "pong" in str(ping).lower()
)
probe["supports_hook"] = frida_ok
probe["hook_tests"]["connect"] = "ok" if frida_ok else "failed"
if frida_ok:
# 性能优化version 与 profile 为独立只读调用,并行执行(墙钟取最大值而非求和),
# 显著缩短 probe 总耗时(原串行 ping+ver+profile ~38s
ver, prof = await asyncio.gather(
_execute_skill(device_id, "wechat", "get_wechat_version", {}, timeout=30, hook_only=True),
_execute_skill(device_id, "wechat", "get_profile", {}, timeout=45, hook_only=True),
)
vdata = ver.get("data") if isinstance(ver.get("data"), dict) else {}
probe["wechat_version"] = str(vdata.get("version") or vdata.get("wechat_version") or ver.get("wechat_version") or "")
prdata = prof.get("data") if isinstance(prof.get("data"), dict) else {}
if isinstance(prdata.get("data"), dict):
probe["profile"] = prdata["data"]
elif prdata:
probe["profile"] = prdata
if not probe["supports_hook"]:
probe["probe_detail"] = (
"Agent WS 在线但 Frida 未 attach请在手机 Termux 运行 Agent127.0.0.1 frida-server无需 Mac ADB"
)
return probe
@router.get("/stability/watch", response_model=dict, tags=["设备稳定性"])
async def stability_watch(device_id: str, samples: int = 1, interval_seconds: float = 0):
"""
设备稳定性采样入口WS/Hook/ADB/耗时统一返回,供 24h 长稳脚本循环调用。
"""
samples = min(max(int(samples or 1), 1), 120)
interval_seconds = min(max(float(interval_seconds or 0), 0), 60)
rows = []
for index in range(samples):
started = time.time()
online = ws_hub.is_online(device_id)
info = ws_hub.get_device_info(device_id) or {}
adb_dev = adb_manager.get_device(device_id)
try:
probe = await _probe_device_via_ws(device_id) if online else {
"supports_hook": False,
"transport": "websocket",
"hook_tests": {"connect": "agent_offline"},
}
hook_ok = bool(probe.get("supports_hook") or probe.get("hook_tests", {}).get("connect") == "ok")
error = ""
except Exception as exc:
probe = {}
hook_ok = False
error = str(exc)[:200]
elapsed_ms = int((time.time() - started) * 1000)
rows.append({
"index": index + 1,
"ts": int(time.time()),
"device_id": device_id,
"device_id_md5": device_id_md5(device_id),
"ws_online": online,
"adb_online": bool(adb_dev),
"adb_serial": getattr(adb_dev, "serial", ""),
"hook_ok": hook_ok,
"wechat_version": probe.get("wechat_version", ""),
"transport": probe.get("transport") or info.get("transport") or "websocket",
"latency_ms": elapsed_ms,
"error": error,
})
if index < samples - 1 and interval_seconds:
await asyncio.sleep(interval_seconds)
ok_count = sum(1 for row in rows if row["ws_online"] and row["hook_ok"])
return {
"code": 200,
"data": {
"device_id": device_id,
"device_id_md5": device_id_md5(device_id),
"samples": rows,
"summary": {
"total": len(rows),
"ok": ok_count,
"success_rate": round(ok_count / len(rows), 4) if rows else 0,
"max_latency_ms": max((row["latency_ms"] for row in rows), default=0),
},
},
}
@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",
contact_limit: int = DEFAULT_CONTACT_PULL_LIMIT,
message_limit: int = DEFAULT_MESSAGE_PULL_LIMIT,
contact_offset: int = 0,
message_offset: int = 0,
):
"""
一次性获取设备的 Hook 数据(联系人/消息/群/标签/资料等)
modules 参数用逗号分隔,可选值:
profile, contacts, groups, messages, labels, moments, accounts, hook_status, device_info
"""
import asyncio
requested = [m.strip() for m in modules.split(",") if m.strip()]
limits = {
"contact_limit": _bounded_limit(contact_limit, DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT),
"message_limit": _bounded_limit(message_limit, DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT),
"contact_offset": _bounded_offset(contact_offset),
"message_offset": _bounded_offset(message_offset),
}
if ws_hub.is_online(device_id):
result = await _fetch_hook_data_via_ws(device_id, requested, limits)
return {"code": 200, "device_id": device_id, "device_id_md5": device_id_md5(device_id), **result}
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: _fetch_hook_data(serial, requested, limits),
)
return {"code": 200, "device_id": device_id, "device_id_md5": device_id_md5(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)
# 易阻塞动作快速失败(如 check_login_state 的 WCDB 加密读会挂 ~58s
# 缩短超时让其尽快落 u2 兜底而非拖垮通道。
_SLOW_ACTION_TIMEOUTS = {"check_login_state": 20}
_action_timeout = _SLOW_ACTION_TIMEOUTS.get(req.action, 120)
result = await _execute_skill(
req.device_id,
req.platform.value,
req.action,
req.params or {},
timeout=_action_timeout,
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"
# 真机铁律 #3/#14诚实传播内层状态禁止把失败/未验证(503/success:False)伪装成 200
inner_code = result.get("code")
inner_success = result.get("success")
if isinstance(payload, dict) and "success" in payload:
inner_success = payload.get("success")
code = inner_code if isinstance(inner_code, int) else 200
if code == 200 and inner_success is False:
code = 503 # hook 未真实成功(如 no_receiver_registered / Frida 主控不可用)
return {"code": code, "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", "risk_action": guard.get("risk_action")}, "channel_used": "none"}
req.content = guard["content"]
# to_id 显示名归一(文件传输助手 → filehelper
_to = (req.to_id or "").strip()
if _to in ("文件传输助手", "File Transfer"):
req.to_id = "filehelper"
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"
# AB-02 静默限流识别success=True 但无 message_id/svr_id 视为可能的静默降权,
# 触发设备级熔断(写类动作冷却 30-60min逐次退避升级不伪造成功
mid = result.get("message_id") or result.get("svr_id") or result.get("msg_id")
if data["success"] and not mid:
cooldown = rate_limiter.trip_silent_throttle(req.device_id)
data["risk"] = "silent_throttle"
data["risk_detail"] = f"发送返回成功但无 message_id/svr_id疑似静默限流已熔断写操作 {cooldown:.0f}s"
logger.warning(f"[message/send] silent_throttle device_id={req.device_id} to_id={req.to_id} channel={channel.value} cooldown={cooldown:.0f}s")
elif data["success"] and mid:
rate_limiter.reset_silent_throttle(req.device_id)
ch_used = result.get("channel_used") or channel.value
return {"code": 200, "data": data, "channel_used": ch_used}
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)
limit = _bounded_limit(req.limit, DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
offset = _bounded_offset(req.offset)
result = await _execute_skill(
req.device_id, req.platform.value, "get_messages",
{"conversation_id": req.conversation_id, "limit": limit, "offset": offset}
)
# 兼容 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,
"count": len(messages),
"requested_limit": limit,
"offset": offset,
"total_count": payload.get("total_count") or payload.get("total"),
"has_more": bool(payload.get("has_more")) or len(messages) >= limit,
},
"channel_used": result.get("_channel_used", "sdk_control")
}
@router.post("/message/sync-since", response_model=dict, tags=["消息管理"])
async def sync_messages_since(req: MessageSyncSinceRequest):
"""按时间增量同步消息,供存客宝/超管轮询拉取。"""
_check_device_online(req.device_id)
limit = _bounded_limit(req.limit, DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
offset = _bounded_offset(req.offset)
result = await _execute_skill(
req.device_id,
req.platform.value,
"get_messages",
{"conversation_id": req.conversation_id, "limit": limit, "offset": offset}
)
payload = _payload_of(result)
messages = _list_from_payload(payload, "messages", "items", "list")
filtered = []
max_ts = int(req.since_time or 0)
for message in messages:
ts = _message_timestamp(message)
if ts >= int(req.since_time or 0):
filtered.append(message)
if ts > max_ts:
max_ts = ts
return {
"code": 200,
"data": {
"messages": filtered,
"count": len(filtered),
"since_time": int(req.since_time or 0),
"next_since_time": max_ts,
"requested_limit": limit,
"offset": offset,
"total_count": payload.get("total_count") or payload.get("total"),
"has_more": len(messages) >= limit and len(filtered) >= limit,
},
"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", "risk_action": guard.get("risk_action")}, "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)))
last_channel = "none"
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)))
normalized_to_id = (to_id or "").strip()
if normalized_to_id in ("文件传输助手", "File Transfer"):
normalized_to_id = "filehelper"
send_req = SendMessageRequest(
device_id=req.device_id,
platform=req.platform,
to_id=normalized_to_id,
content=req.content,
msg_type=req.msg_type,
media_url=req.media_url,
timeout_seconds=per_msg_timeout,
)
result = await _send_via_hook(send_req)
last_channel = result.get("channel_used") or last_channel
if result.get("success"):
sent.append({"to_id": normalized_to_id, "message_id": result.get("message_id")})
else:
failed.append({"to_id": normalized_to_id, "error": result.get("error") or "unknown"})
return {
"code": 200,
"data": {"sent": sent, "failed": failed, "total": len(req.to_ids)},
"channel_used": last_channel
}
@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", "risk_action": guard.get("risk_action")}
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", "risk_action": guard.get("risk_action")}, "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", "risk_action": guard.get("risk_action")}
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 = DEFAULT_CONTACT_PULL_LIMIT, offset: int = 0):
"""获取联系人列表存客宝字段完整display_name / wechat_id / tags 等)"""
from services.wechat_contact_normalizer import normalize_wechat_contacts
_check_device_online(device_id)
limit = _bounded_limit(limit, DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT)
offset = _bounded_offset(offset)
result = await _execute_skill(
device_id, platform.value, "get_contacts",
{"limit": limit, "offset": offset}
)
payload = result.get("data") if result.get("data") is not None else result
raw = payload.get("contacts", [])
contacts = normalize_wechat_contacts(raw) if platform == Platform.WECHAT else raw
return {
"code": 200,
"data": {
"contacts": contacts,
"count": len(contacts),
"requested_limit": limit,
"offset": offset,
"total_count": payload.get("total_count") or payload.get("total") or (payload.get("_diag") or {}).get("filtered_total"),
"raw_total_count": (payload.get("_diag") or {}).get("rcontact_total"),
"has_more": bool(payload.get("has_more")) or len(contacts) >= limit,
},
"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", "risk_action": guard.get("risk_action")}
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 = Platform.WECHAT, limit: int = 100, offset: int = 0):
"""获取群聊列表"""
_check_device_online(device_id)
limit = _bounded_limit(limit, 100, 500)
offset = _bounded_offset(offset)
result = await _execute_skill(
device_id, platform.value, "get_groups",
{"limit": limit, "offset": offset}
)
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, group_id: str, platform: Platform = Platform.WECHAT):
"""获取群成员列表"""
_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, limit: int = 100, offset: int = 0):
"""获取标签列表"""
_check_device_online(device_id)
limit = _bounded_limit(limit, 100, 500)
offset = _bounded_offset(offset)
result = await _execute_skill(
device_id, platform.value, "get_tags", {"limit": limit, "offset": offset}
)
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"], "risk_action": guard.get("risk_action"), "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", "risk_action": guard.get("risk_action")}
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", "risk_action": guard.get("risk_action")}
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)
try:
result = await asyncio.wait_for(
_execute_skill(
req.device_id, req.platform.value, "get_moments",
{"user_id": req.user_id, "limit": req.limit},
timeout=25,
hook_only=req.platform == Platform.WECHAT,
),
timeout=30,
)
except asyncio.TimeoutError:
result = {
"code": 503,
"data": {
"success": False,
"error": "朋友圈列表读取超时SnsMicroMsg.db/WCDB2 适配待完成",
"blocked_reason": "sns_wcdb2_timeout",
"moments": [],
"count": 0,
},
"_channel_used": "websocket/hook(timeout)",
}
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("/customer/profile-bundle", response_model=dict, tags=["存客宝对接"])
async def get_customer_profile_bundle(
device_id: str,
platform: Platform = Platform.WECHAT,
keyword: str = "",
user_id: str = "",
limit: int = 50,
contact_limit: int = DEFAULT_CONTACT_PULL_LIMIT,
message_limit: int = DEFAULT_MESSAGE_PULL_LIMIT,
group_limit: int = 100,
tag_limit: int = 100,
contact_offset: int = 0,
message_offset: int = 0,
group_offset: int = 0,
tag_offset: int = 0,
):
"""
客户画像聚合包:资料、联系人、标签、群、最近消息一次返回。
面向存客宝/超管 BFF减少多接口拼装成本底层仍走真机 Hook/Agent。
"""
from services.wechat_contact_normalizer import normalize_wechat_contacts
_check_device_online(device_id)
limit = _bounded_limit(limit, 50, 500)
contact_limit = _bounded_limit(contact_limit, DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT)
message_limit = _bounded_limit(message_limit, DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
group_limit = _bounded_limit(group_limit, 100, 500)
tag_limit = _bounded_limit(tag_limit, 100, 500)
contact_offset = _bounded_offset(contact_offset)
message_offset = _bounded_offset(message_offset)
group_offset = _bounded_offset(group_offset)
tag_offset = _bounded_offset(tag_offset)
query = user_id or keyword
tasks = {
"profile": _execute_skill(device_id, platform.value, "get_profile", {}, timeout=45),
"contacts": _execute_skill(device_id, platform.value, "get_contacts", {"limit": contact_limit, "offset": contact_offset}, timeout=90),
"groups": _execute_skill(device_id, platform.value, "get_groups", {"limit": group_limit, "offset": group_offset}, timeout=45),
"tags": _execute_skill(device_id, platform.value, "get_tags", {"limit": tag_limit, "offset": tag_offset}, timeout=45),
"messages": _execute_skill(device_id, platform.value, "get_messages", {"limit": message_limit, "offset": message_offset}, timeout=90),
}
if query:
tasks["contact_search"] = _execute_skill(
device_id, platform.value, "search_contact", {"keyword": query}, timeout=45
)
if user_id:
tasks["friend_info"] = _execute_skill(
device_id, platform.value, "get_friend_info", {"user_id": user_id}, timeout=45
)
names = list(tasks.keys())
raw_results = await asyncio.gather(*tasks.values(), return_exceptions=True)
results = {}
channels = {}
errors = {}
for name, result in zip(names, raw_results):
if isinstance(result, Exception):
results[name] = {}
errors[name] = str(result)[:200]
channels[name] = "error"
continue
results[name] = _payload_of(result)
channels[name] = result.get("_channel_used", "sdk_control") if isinstance(result, dict) else "unknown"
contacts = _list_from_payload(results.get("contacts", {}), "contacts")
contacts = normalize_wechat_contacts(contacts) if platform == Platform.WECHAT else contacts
matched_contacts = [c for c in contacts if _matches_contact(c, query)][:limit] if query else contacts[:limit]
groups = _list_from_payload(results.get("groups", {}), "groups", "chatrooms")
tags = _list_from_payload(results.get("tags", {}), "tags", "labels")
messages = _list_from_payload(results.get("messages", {}), "messages")[:limit]
contact_payload = results.get("contacts", {})
message_payload = results.get("messages", {})
contact_total = contact_payload.get("total_count") or contact_payload.get("total") or (contact_payload.get("_diag") or {}).get("filtered_total") or len(contacts)
message_items = _list_from_payload(message_payload, "messages")
message_total = message_payload.get("total_count") or message_payload.get("total") or len(message_items)
return {
"code": 200,
"data": {
"device_id": device_id,
"device_id_md5": device_id_md5(device_id),
"query": {
"keyword": keyword,
"user_id": user_id,
"limit": limit,
"contact_limit": contact_limit,
"message_limit": message_limit,
"group_limit": group_limit,
"tag_limit": tag_limit,
"contact_offset": contact_offset,
"message_offset": message_offset,
"group_offset": group_offset,
"tag_offset": tag_offset,
},
"profile": results.get("profile", {}),
"contacts": matched_contacts,
"contact_count": contact_total,
"returned_contact_count": len(contacts),
"matched_contact_count": len(matched_contacts),
"contact_requested_limit": contact_limit,
"contact_offset": contact_offset,
"contact_total_count": contact_total,
"contact_raw_total_count": (contact_payload.get("_diag") or {}).get("rcontact_total"),
"contacts_has_more": bool(contact_payload.get("has_more")) or len(contacts) >= contact_limit,
"groups": groups[:limit],
"group_count": len(groups),
"group_requested_limit": group_limit,
"group_offset": group_offset,
"tags": tags[:limit],
"tag_count": len(tags),
"tag_requested_limit": tag_limit,
"tag_offset": tag_offset,
"recent_messages": messages,
"message_count": message_total,
"returned_message_count": len(message_items),
"message_requested_limit": message_limit,
"message_offset": message_offset,
"message_total_count": message_total,
"messages_has_more": bool(message_payload.get("has_more")) or len(message_items) >= message_limit,
"contact_search": results.get("contact_search", {}),
"friend_info": results.get("friend_info", {}),
"errors": errors,
},
"channel_used": channels,
}
@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.post("/account/unblock-customer-service", response_model=dict, tags=["账号安全"])
async def unblock_via_customer_service(
device_id: str,
platform: Platform,
reason: str = "",
phone: str = "",
wxid: str = "",
max_rounds: int = 20,
chat_interval_sec: int = 8,
use_web_search: bool = True,
):
"""
联系客服解封 — 全自动链路 + AI 对话
流程: 设置→账号与安全→微信安全中心→联系客服→客服会话→AI 持续软磨硬泡
返回 data 含 `session.turns[]`(每轮客服消息+AI 回复+发送状态)与 `final_status`
success / reject / timeout / fallback / error
"""
_check_device_online(device_id)
params = {
"reason": reason,
"phone": phone,
"wxid": wxid,
"max_rounds": max_rounds,
"chat_interval_sec": chat_interval_sec,
"use_web_search": use_web_search,
}
run_timeout = max(180, int(max_rounds) * (int(chat_interval_sec) + 12) + 180)
result = await _execute_skill(
device_id,
platform.value,
"unblock_via_customer_service",
params,
timeout=run_timeout,
)
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
if platform == Platform.WECHAT and isinstance(payload, dict) and payload.get("success") is False:
# 视频号尚无稳定内部 RPC设备有 ADB 时使用真实 UI 读取兜底,禁止把广播占位返回成成功。
adb_device = adb_manager.get_device(device_id)
if adb_device and adb_device.is_online():
adb_result = await _execute_via_adb(
adb_device,
platform.value,
"get_video_list",
{"limit": min(max(limit, 1), 20)},
timeout=60,
)
if adb_result.get("success"):
return {"code": 200, "data": adb_result, "channel_used": "adb/sdk_control"}
payload = {
**payload,
"adb_fallback": adb_result,
}
return {"code": 503, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
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 = Body(...),
platform: Platform = Body(Platform.WECHAT),
image_base64: str = Body("", description="二维码图片 base64PNG/JPG可含 data: 前缀)"),
image_url: str = Body("", description="二维码图片可下载 URL"),
):
"""从图片识别二维码(支持传入 image_base64 / image_url缺省则识别当前相册选图页"""
_check_device_online(device_id)
params = {}
if image_base64:
params["image_base64"] = image_base64
if image_url:
params["image_url"] = image_url
result = await _execute_skill(device_id, platform.value, "extract_qr_from_image", 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("/scan/add-friend-from-image", response_model=dict, tags=["扫一扫"])
async def add_friend_from_image(
device_id: str = Body(...),
platform: Platform = Body(Platform.WECHAT),
image_base64: str = Body("", description="好友二维码图片 base64PNG/JPG可含 data: 前缀)"),
image_url: str = Body("", description="好友二维码图片可下载 URL"),
verify_message: str = Body("", description="加好友打招呼语(可选)"),
):
"""AI/BFF 下发二维码图片 → 真机识别 → 自动加好友WP-WX-04 04.1 契约层)。
执行链Agent 侧 add_friend_from_image保存图片到相册 → 微信扫一扫从相册选图
→ 识别 qr_content → add_friend_by_qr(verify_message)。
失败诚实返回(识别失败/二维码过期/已是好友/503 Hook 未附着),禁止 mock success。
"""
if not image_base64 and not image_url:
raise HTTPException(status_code=400, detail="必须提供 image_base64 或 image_url 之一")
_check_device_online(device_id)
params = {"verify_message": verify_message}
if image_base64:
params["image_base64"] = image_base64
if image_url:
params["image_url"] = image_url
result = await _execute_skill(device_id, platform.value, "add_friend_from_image", 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("/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", "risk_action": guard.get("risk_action")}
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_sdkADB/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 = {
"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),
}
timeout = req.timeout_seconds or settings.MESSAGE_SEND_TIMEOUT
result = await ws_hub.send_command(
req.device_id,
{
"type": "execute",
"data": {
"script": req.platform.value,
"action": "send_message",
"params": hook_params,
"hook_only": True,
},
},
timeout=timeout,
)
payload = result.get("data") if isinstance(result.get("data"), dict) else {}
ok = result.get("code") == 200 and (
payload.get("success") is True or result.get("success") is True
)
if ok:
mid = payload.get("message_id") or result.get("message_id")
return {
"success": True,
"message_id": mid,
"channel_used": "websocket/frida",
}
err = payload.get("error") or result.get("message") or "WebSocket Hook 发送失败"
logger.warning(f"[_send_via_hook] WS Frida 失败: {result}")
if getattr(settings, "WECHAT_WS_HOOK_ONLY", True):
if _should_degrade_hook_send(err, result):
sdk_result = await _send_via_sdk(req)
sdk_result.setdefault(
"channel_used",
"websocket/hook(degraded_to_u2)" if sdk_result.get("success") else "websocket/hook+u2(failed)",
)
if not sdk_result.get("success"):
sdk_result.setdefault("hook_error", err)
return sdk_result
return {"success": False, "error": err, "channel_used": "websocket/hook(failed)"}
except Exception as e:
logger.warning(f"[_send_via_hook] Hook 通道异常: {e}")
if getattr(settings, "WECHAT_WS_HOOK_ONLY", True):
return {"success": False, "error": str(e), "channel_used": "websocket/hook(failed)"}
ws_hook_only = getattr(settings, "WECHAT_WS_HOOK_ONLY", True)
if ws_hook_only:
if mode != "websocket" or not ws_hub.is_online(req.device_id):
return {
"success": False,
"error": "WebSocket Agent 未在线WECHAT_WS_HOOK_ONLY",
"channel_used": "websocket/offline",
}
elif 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) and not ws_hook_only:
return {"success": False, "error": "微信后端模式要求 Frida Hook 可用", "channel_used": "none"}
if ws_hook_only:
return {
"success": False,
"error": "WebSocket Hook 发送失败",
"channel_used": "websocket/hook(failed)",
}
result = await _send_via_sdk(req)
if result.get("success"):
result["channel_used"] = "hook(degraded_to_sdk)"
return result
def _should_degrade_hook_send(error: str, result: dict) -> bool:
"""Hook 无法真实发送时,允许转到同一真机 Agent 的 u2 UI 兜底。"""
text = f"{error or ''} {result or ''}"
return any(
token in text
for token in (
"no_receiver_registered",
"接收端不可验证",
"sendMessage",
"RPC 返回 success=false",
)
)
# 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
async def _fetch_hook_data_via_ws(device_id: str, modules: list, limits: dict = None) -> dict:
"""通过在线 Agent WebSocket 批量获取 Hook 数据。"""
limits = limits or {}
contact_limit = _bounded_limit(limits.get("contact_limit"), DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT)
message_limit = _bounded_limit(limits.get("message_limit"), DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
contact_offset = _bounded_offset(limits.get("contact_offset"))
message_offset = _bounded_offset(limits.get("message_offset"))
module_map = {
"profile": ("get_profile", {}),
"contacts": ("get_contacts", {"limit": contact_limit, "offset": contact_offset}),
"groups": ("get_groups", {}),
"messages": ("get_messages", {"limit": message_limit, "offset": message_offset}),
"labels": ("get_tags", {}),
"tags": ("get_tags", {}),
"moments": ("get_moments", {"limit": 10}),
"accounts": ("get_official_accounts", {}),
"hook_status": ("get_hook_status", {}),
"device_info": ("get_device_info", {}),
"wechat_version": ("get_wechat_version", {}),
"process_info": ("get_process_info", {}),
"storage_info": ("get_storage_info", {}),
"network_info": ("get_network_info", {}),
"login_state": ("check_login_state", {}),
"favorites": ("get_favorites", {"limit": 20}),
}
async def fetch_one(module: str):
action_spec = module_map.get(module)
if not action_spec:
return module, {"success": False, "error": f"未知模块: {module}"}, False
action, params = action_spec
try:
result = await _execute_skill(
device_id,
"wechat",
action,
params,
timeout=45,
hook_only=True,
)
payload = result.get("data") if isinstance(result.get("data"), dict) else result
payload = payload if isinstance(payload, dict) else {"data": payload}
ok = result.get("code") == 200 and payload.get("success") is not False
if payload.get("success") is True or result.get("success") is True:
ok = True
payload.setdefault("action", action)
payload.setdefault("channel_used", result.get("_channel_used") or result.get("channel") or "websocket/hook")
return module, payload, ok
except Exception as exc:
return module, {"success": False, "error": str(exc)[:200], "action": action}, False
data = {}
ok_modules = []
requested = modules or list(module_map.keys())
results = await asyncio.gather(*(fetch_one(module) for module in requested))
for module, payload, ok in results:
data[module] = payload
if ok:
ok_modules.append(module)
return {
"success": bool(ok_modules),
"data": data,
"modules_fetched": ok_modules,
"modules_requested": requested,
"limits": {
"contact_limit": contact_limit,
"message_limit": message_limit,
"contact_offset": contact_offset,
"message_offset": message_offset,
},
"transport": "websocket",
"channel_used": "websocket/hook",
"partial_success": 0 < len(ok_modules) < len(requested),
}
def _fetch_hook_data(device_serial: str, modules: list, limits: dict = None) -> dict:
"""通过本地 Frida 批量获取 Hook 数据"""
import re
def to_snake(name):
return re.sub(r'(?<=[a-z0-9])([A-Z])', r'_\1', name).lower()
limits = limits or {}
contact_limit = _bounded_limit(limits.get("contact_limit"), DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT)
message_limit = _bounded_limit(limits.get("message_limit"), DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
contact_offset = _bounded_offset(limits.get("contact_offset"))
message_offset = _bounded_offset(limits.get("message_offset"))
MODULE_MAP = {
"profile": ("getProfile", {}),
"contacts": ("getContacts", {"limit": contact_limit, "offset": contact_offset}),
"groups": ("getGroups", {}),
"messages": ("getMessages", {"limit": message_limit, "offset": message_offset}),
"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()),
"limits": {
"contact_limit": contact_limit,
"message_limit": message_limit,
"contact_offset": contact_offset,
"message_offset": message_offset,
},
}
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 直连设备执行 HookADB 模式专用)"""
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)
forced = (req.channel or "").strip().lower()
# 显式 sdk_control 时优先走 ADB 引擎(避免僵尸 WS 占通道导致 60s 超时)
if forced == "sdk_control":
adb_dev = adb_manager.get_device(req.device_id)
if adb_dev and adb_dev.is_online():
mode = "adb"
timeout = max(timeout, 90)
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)
payload = result.get("data") if isinstance(result.get("data"), dict) else {}
success = payload.get("success") is True or result.get("success") is True
if result.get("code") == 200 and success:
return {
"success": True,
"message_id": payload.get("message_id") or result.get("message_id"),
"channel_used": f"websocket/{result.get('channel') or payload.get('channel') or 'u2'}",
}
err = payload.get("error") or payload.get("frida_error") or 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,
"channel_used": f"websocket/{result.get('channel') or payload.get('channel') or 'failed'}",
}
elif mode == "adb":
adb_device = adb_manager.get_device(req.device_id)
if not adb_device:
return {"success": False, "error": "ADB设备不可用"}
return await _execute_via_adb(adb_device, req.platform.value, "send_message", params)
return {"success": False, "error": "设备不在线"}
async def _send_via_agent(req: SendMessageRequest) -> dict:
"""通过AI Agent发送WebSocket 下发 agent_executeADB 模式由服务端解析后执行 action 序列"""
platform_names = {
Platform.WECHAT: "微信",
Platform.DOUYIN: "抖音",
Platform.XHS: "小红书",
Platform.XIANYU: "闲鱼",
Platform.SOUL: "Soul"
}
task = f"打开{platform_names.get(req.platform, req.platform.value)},找到联系人'{req.to_id}',发送消息:{req.content}"
mode = _get_device_mode(req.device_id)
if mode == "websocket":
# 设备端已有 agent_execute 处理,直接下发任务
result = await ws_hub.send_command(req.device_id, {
"type": "agent_execute",
"data": {"task": task, "llm_provider": "deepseek", "max_steps": 30}
}, timeout=120)
if result.get("code") == 200 and result.get("data"):
return {
"success": True,
"message_id": result.get("data", {}).get("message_id") or f"agent_{int(__import__('time').time() * 1000)}"
}
return {"success": False, "error": result.get("message", "AI Agent 执行失败")}
if mode == "adb":
# ADB 模式:服务端解析意图后通过 ADB 执行 action 序列
parse_result = await ai_agent_service.process_voice_command(task, req.device_id)
if not parse_result.get("success") or not parse_result.get("actions"):
return {"success": False, "error": parse_result.get("message", "无法解析指令")}
adb_device = adb_manager.get_device(req.device_id)
if not adb_device or not adb_device.is_online():
return {"success": False, "error": "ADB 设备不可用"}
ok, err = await _execute_actions_via_adb(adb_device, parse_result["actions"])
if ok:
return {"success": True, "message_id": f"agent_adb_{int(__import__('time').time() * 1000)}"}
return {"success": False, "error": err or "ADB 执行失败"}
return {"success": False, "error": "设备离线"}
# =============================================================================
# 三十、自动注册 & 设备初始化
# =============================================================================
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}
@router.post("/account/wechat/check-login-state", response_model=dict, tags=["注册/登录"])
async def wechat_check_login_state_agent(device_id: str, platform: Platform):
"""设备端 u2 检测微信是否已登录"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "check_login_state", {})
payload = result.get("data") if isinstance(result.get("data"), dict) and result.get("data") else result
code = result.get("code", 200)
return {"code": code, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
@router.post("/account/wechat/login-by-password", response_model=dict, tags=["注册/登录"])
async def wechat_login_by_password(
device_id: str,
platform: Platform,
account: str = "",
password: str = "",
phone: str = "",
):
"""微信号/手机号 + 密码登录u2 自动化)"""
_check_device_online(device_id)
params = {
"account": account or phone,
"phone": phone,
"password": password,
}
result = await _execute_skill(
device_id, platform.value, "login_by_password", params, timeout=120
)
payload = result.get("data") if isinstance(result.get("data"), dict) and result.get("data") else result
code = result.get("code", 200)
return {"code": code, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
@router.post("/account/wechat/ensure-login", response_model=dict, tags=["注册/登录"])
async def wechat_ensure_login(
device_id: str,
platform: Platform,
account: str = "",
password: str = "",
phone: str = "",
):
"""未登录则按参数或 sdk/config/wechat_login.yaml 自动登录"""
_check_device_online(device_id)
params = {
"account": account or phone,
"phone": phone,
"password": password,
}
result = await _execute_skill(
device_id, platform.value, "ensure_logged_in", params, timeout=120
)
payload = result.get("data") if isinstance(result.get("data"), dict) and result.get("data") else result
code = result.get("code", 200)
return {"code": code, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
# ========== 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", "")),
}