Files
workphone-sdk/sdk/app/services/device_transport.py

267 lines
9.1 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.

"""
设备统一传输层 — 机擎默认强制规则WebSocket 无线主控优先。
所有设备控制hook/execute、message、probe、Skill须经本模块选路与下发。
禁止在业务路由中绕过本层直接调用 ADB / 主机 frida-ps / Mac ADB forward Agent。
优先级WORKPHONE_WS_FIRST=1默认
1. WebSocket Agent + 设备本机 FridaHook
2. ADB 静默(仅运维兜底或显式 u2 动作)
3. scrcpy / AI Agent 编排(依赖底层通道)
真源文档:开发文档/5、接口/02-业务对接/WebSocket无线主控口径.md
"""
from __future__ import annotations
import logging
from enum import Enum
from typing import Any, Dict, Optional
from fastapi import HTTPException
logger = logging.getLogger(__name__)
class TransportChannel(str, Enum):
WEBSOCKET = "websocket"
HOOK = "hook"
ADB = "adb"
OFFLINE = "offline"
# 允许在无 WS 时降级 ADB 的微信动作(客服解封等,见 unified.WECHAT_U2_ONLY_ACTIONS
# 登录/退出/注册/切号属纯 UI 流程Frida RPC 仅广播 intent设备无接收端时返回
# no_receiver_registered真机铁律禁止假成功必须放行 u2/ADB 真实 UI 兜底,否则功能不可用。
WECHAT_U2_ONLY_ACTIONS = frozenset({
"unblock_via_customer_service",
"unblock_account",
"unblock_self",
"unblock_appeal",
"unblock_with_sms",
"set_nickname",
"set_signature",
"set_avatar",
"set_sex",
"set_region",
"set_what_up",
"change_password",
"bind_phone",
"unbind_phone",
"get_login_devices",
"remove_login_device",
"enable_fingerprint",
"set_account_protection",
"login_by_password",
"login_by_sms",
"register_account",
"auto_register",
"logout",
"switch_account",
"ensure_logged_in",
"send_red_packet",
"receive_red_packet",
"send_transfer",
"receive_transfer",
"receive_payment",
"get_wallet_balance",
"get_transaction_history",
# UI/展示类素材Frida 仅 intent 占位,须 u2 真导航+截图)
"generate_my_qr_code",
"show_my_qr",
"generate_group_qr_code",
"show_payment_code",
"scan_qr_code",
"add_friend_by_qr",
"voice_call",
"video_call",
"share_real_time_location",
"download_file",
"mass_send",
"batch_send",
"add_favorite",
"delete_favorite",
"follow_official_account",
"unfollow_official_account",
"open_mini_program",
"get_recent_mini_programs",
"share_mini_program",
"send_voice",
"send_file",
"send_location",
"send_card",
"send_emoji",
"add_custom_emoji",
"add_to_float",
"remove_from_float",
"set_privacy",
"set_notification",
"clear_chat_history",
"set_chat_background",
"set_do_not_disturb",
"pin_chat",
"clear_cache",
"check_for_update",
"delete_moments",
"browse_channels",
"like_channel_video",
"comment_channel_video",
"follow_channel",
"unfollow_channel",
"share_channel_video",
# forward_message / forward_multiple / revoke_message 已实现真 Frida RPC
# (文本转发复用 _sendMessageInternal / 撤回 DB 资格预检),走 Frida 主通道,不再强制 u2
"get_official_account_articles",
"reply_comment",
})
class DeviceTransport:
"""单设备传输策略与 WS 下发封装。"""
@staticmethod
def _settings():
from config import settings
return settings
def ws_first_enabled(self) -> bool:
return bool(getattr(self._settings(), "WORKPHONE_WS_FIRST", True))
def ws_hook_only(self, platform: str) -> bool:
if platform != "wechat":
return self.ws_first_enabled()
return bool(getattr(self._settings(), "WECHAT_WS_HOOK_ONLY", True))
def host_adb_probe_enabled(self) -> bool:
return bool(getattr(self._settings(), "WORKPHONE_HOST_ADB_PROBE", False))
def is_ws_online(self, device_id: str) -> bool:
from services.ws_hub import ws_hub
return ws_hub.is_online(device_id)
def resolve_mode(self, device_id: str, platform: str = "wechat", action: str = "") -> str:
"""
解析当前设备应使用的传输模式。
无线主控WS 在线 → websocket否则 offlineu2 专属动作除外)。
"""
if self.is_ws_online(device_id):
return TransportChannel.WEBSOCKET.value
if self.ws_first_enabled():
if platform == "wechat" and action in WECHAT_U2_ONLY_ACTIONS:
from services.adb_device import adb_manager
dev = adb_manager.get_device(device_id)
if dev and dev.is_online():
return TransportChannel.ADB.value
return TransportChannel.OFFLINE.value
from services.adb_device import adb_manager
adb_dev = adb_manager.get_device(device_id)
if adb_dev and adb_dev.is_online():
return TransportChannel.ADB.value
from services.connection_priority import connection_priority
ch = connection_priority.choose_execution_channel(device_id)
if ch == "agent":
return TransportChannel.WEBSOCKET.value
if ch and ch != "offline":
return ch
return TransportChannel.OFFLINE.value
def check_device_online(self, device_id: str, platform: str = "wechat", action: str = "") -> str:
"""按策略检查设备是否可执行业务;不可用时抛 HTTPException。"""
mode = self.resolve_mode(device_id, platform, action)
if mode == TransportChannel.OFFLINE.value:
detail = (
f"设备 WebSocket 未连接: {device_id}"
"无线主控请在手机 Termux 启动 Agent 连接 ws://<sdk>/ws/device/{device_id}"
)
if self.ws_first_enabled():
raise HTTPException(status_code=503, detail=detail)
raise HTTPException(status_code=503, detail=f"设备不在线: {device_id}")
return "agent" if mode == TransportChannel.WEBSOCKET.value else mode
def offline_payload(self, device_id: str, reason: str = "") -> Dict[str, Any]:
msg = reason or (
"WebSocket Agent 未在线,请启动设备端 Termux Agent "
f"连接 ws://<sdk>/ws/device/{device_id}"
)
return {
"code": 503,
"success": False,
"message": msg,
"transport": TransportChannel.WEBSOCKET.value,
"_channel_used": "websocket/offline",
}
def policy_meta(self, device_id: str) -> Dict[str, Any]:
from services.device_id_util import device_id_md5
from services.ws_hub import ws_hub
info = ws_hub.get_device_info(device_id) or {}
return {
"device_id": device_id,
"device_id_md5": device_id_md5(device_id),
"transport": TransportChannel.WEBSOCKET.value if self.is_ws_online(device_id) else TransportChannel.OFFLINE.value,
"ws_first": self.ws_first_enabled(),
"ws_online": self.is_ws_online(device_id),
"frida_available": bool(info.get("frida_available")),
"agent_on_device": bool(info.get("on_device") or info.get("WP_AGENT_ON_DEVICE")),
}
async def execute_via_ws(
self,
device_id: str,
platform: str,
action: str,
params: dict,
*,
timeout: int = 120,
hook_only: bool = False,
) -> Dict[str, Any]:
"""经 WebSocket 统一下发 execute唯一推荐业务入口"""
from services.ws_hub import ws_hub
# 微信内部动作优先走宝塔侧真实 Frida SessionAndroid/system/device
# 仍走手机 WebSocket避免同一设备 ID 的两个 Agent 抢占连接。
if platform == "wechat":
from services.server_frida_bridge import server_frida_bridge
if server_frida_bridge.enabled_for(device_id):
return await server_frida_bridge.execute(device_id, action, params)
if not self.is_ws_online(device_id):
return self.offline_payload(device_id)
result = await ws_hub.send_command(
device_id,
{
"type": "execute",
"data": {
"script": platform,
"action": action,
"params": params,
"hook_only": hook_only,
},
},
timeout=timeout,
)
payload = result.get("data") if isinstance(result.get("data"), dict) else {}
agent_channel = (
result.get("channel")
or payload.get("channel")
or ("hook" if hook_only and payload.get("success") else "u2")
)
result["_channel_used"] = f"websocket/{agent_channel}"
result.setdefault("transport", TransportChannel.WEBSOCKET.value)
return result
def should_force_hook_only(self, platform: str, action: str, hook_only: bool) -> bool:
if hook_only:
return True
if platform == "wechat" and getattr(self._settings(), "WECHAT_BACKEND_ONLY", True):
return action not in WECHAT_U2_ONLY_ACTIONS
return False
device_transport = DeviceTransport()