273 lines
7.3 KiB
Python
273 lines
7.3 KiB
Python
"""
|
||
工作手机SDK v3.0 - ADB设备路由
|
||
直接ADB控制模式(支持模拟器和真机)
|
||
|
||
所有端点使用 def(非 async def),FastAPI 自动在线程池中执行,
|
||
避免同步 ADB shell 命令阻塞事件循环。
|
||
"""
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel
|
||
from typing import Optional
|
||
import sys
|
||
import os
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from services.adb_device import adb_manager
|
||
from services.ws_hub import ws_hub
|
||
|
||
router = APIRouter(prefix="/api/v3/adb", tags=["ADB设备控制"])
|
||
|
||
|
||
async def _ws_execute(serial: str, action: str, params: Optional[dict] = None, timeout: int = 30) -> Optional[dict]:
|
||
"""ADB 兼容路由兜底:serial 为逻辑 device_id 且 WS 在线时转发到 Agent。"""
|
||
if not ws_hub.is_online(serial):
|
||
return None
|
||
result = await ws_hub.send_command(
|
||
serial,
|
||
{
|
||
"type": "execute",
|
||
"data": {
|
||
"action": action,
|
||
"params": params or {},
|
||
},
|
||
},
|
||
timeout=timeout,
|
||
)
|
||
code = result.get("code", 200)
|
||
return {
|
||
"code": code,
|
||
"message": result.get("message", "success" if code == 200 else "failed"),
|
||
"data": result.get("data", {}),
|
||
"channel_used": "websocket/agent",
|
||
}
|
||
|
||
|
||
def _raise_not_found():
|
||
raise HTTPException(status_code=404, detail="设备不存在")
|
||
|
||
|
||
# ========== 请求模型 ==========
|
||
|
||
class ClickRequest(BaseModel):
|
||
x: int
|
||
y: int
|
||
|
||
|
||
class ClickTextRequest(BaseModel):
|
||
text: str
|
||
timeout: float = 10
|
||
|
||
|
||
class InputRequest(BaseModel):
|
||
text: str
|
||
clear: bool = True
|
||
|
||
|
||
class SwipeRequest(BaseModel):
|
||
direction: str
|
||
scale: float = 0.5
|
||
|
||
|
||
class AppRequest(BaseModel):
|
||
package: str
|
||
|
||
|
||
class KeyRequest(BaseModel):
|
||
key: str
|
||
|
||
|
||
# ========== 设备管理 ==========
|
||
|
||
@router.get("/devices")
|
||
def list_devices():
|
||
"""获取所有ADB设备"""
|
||
devices = adb_manager.list_devices()
|
||
return {"code": 200, "data": devices}
|
||
|
||
|
||
@router.get("/scan")
|
||
@router.post("/scan")
|
||
def scan_devices():
|
||
"""扫描设备(含未授权设备的完整信息)"""
|
||
serials = adb_manager.scan_devices()
|
||
all_devices = adb_manager.scan_all_devices()
|
||
return {
|
||
"code": 200,
|
||
"data": {
|
||
"devices": serials,
|
||
"all_devices": all_devices,
|
||
"authorized_count": len(serials),
|
||
"total_count": len(all_devices),
|
||
},
|
||
}
|
||
|
||
|
||
@router.get("/devices/{serial}")
|
||
async def get_device_info(serial: str):
|
||
"""获取设备详情"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
info = ws_hub.get_device_info(serial)
|
||
if info:
|
||
return {"code": 200, "data": {**info, "channel_used": "websocket/agent"}}
|
||
_raise_not_found()
|
||
|
||
info = device.get_info()
|
||
info["status"] = "online" if device.is_online() else "offline"
|
||
return {"code": 200, "data": info}
|
||
|
||
|
||
# ========== 基础操作 ==========
|
||
|
||
@router.post("/devices/{serial}/screenshot")
|
||
async def screenshot(serial: str):
|
||
"""截图"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "screenshot", timeout=12)
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.screenshot()
|
||
|
||
|
||
@router.post("/devices/{serial}/click")
|
||
async def click(serial: str, req: ClickRequest):
|
||
"""点击坐标"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "click", {"x": req.x, "y": req.y})
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.click(req.x, req.y)
|
||
|
||
|
||
@router.post("/devices/{serial}/click-text")
|
||
async def click_text(serial: str, req: ClickTextRequest):
|
||
"""点击文字"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "click_text", {"text": req.text, "timeout": req.timeout})
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.click_text(req.text, req.timeout)
|
||
|
||
|
||
@router.post("/devices/{serial}/input")
|
||
async def input_text(serial: str, req: InputRequest):
|
||
"""输入文字"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "input", {"text": req.text, "clear": req.clear})
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.input_text(req.text, req.clear)
|
||
|
||
|
||
@router.post("/devices/{serial}/swipe")
|
||
async def swipe(serial: str, req: SwipeRequest):
|
||
"""滑动"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "swipe", {"direction": req.direction, "scale": req.scale})
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.swipe(req.direction, req.scale)
|
||
|
||
|
||
@router.post("/devices/{serial}/key")
|
||
async def press_key(serial: str, req: KeyRequest):
|
||
"""按键"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "press_key", {"key": req.key})
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.press_key(req.key)
|
||
|
||
|
||
# ========== APP操作 ==========
|
||
|
||
@router.post("/devices/{serial}/app/start")
|
||
async def start_app(serial: str, req: AppRequest):
|
||
"""启动APP"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "app_start", {"package": req.package}, timeout=45)
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.start_app(req.package)
|
||
|
||
|
||
@router.post("/devices/{serial}/app/stop")
|
||
async def stop_app(serial: str, req: AppRequest):
|
||
"""停止APP"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "app_stop", {"package": req.package}, timeout=45)
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.stop_app(req.package)
|
||
|
||
|
||
@router.get("/devices/{serial}/app/current")
|
||
async def current_app(serial: str):
|
||
"""获取当前APP"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "get_foreground", timeout=15)
|
||
if result:
|
||
return result
|
||
_raise_not_found()
|
||
|
||
return device.current_app()
|
||
|
||
|
||
@router.get("/devices/{serial}/app/list")
|
||
def list_apps(serial: str):
|
||
"""获取已安装APP"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
raise HTTPException(status_code=404, detail="设备不存在")
|
||
|
||
return device.installed_apps()
|
||
|
||
|
||
# ========== UI ==========
|
||
|
||
@router.get("/devices/{serial}/ui-tree")
|
||
async def get_ui_tree(serial: str):
|
||
"""获取UI树(XML 已做 JSON-safe 转义)"""
|
||
device = adb_manager.get_device(serial)
|
||
if not device:
|
||
result = await _ws_execute(serial, "ui_tree", timeout=20)
|
||
if result:
|
||
xml = result.get("data", {}).get("xml", "")
|
||
if isinstance(xml, str):
|
||
result["data"]["xml"] = xml.replace("\x00", "").replace("\r", "")
|
||
return result
|
||
_raise_not_found()
|
||
|
||
result = device.get_ui_tree()
|
||
if isinstance(result, dict) and "data" in result:
|
||
xml = result["data"].get("xml", "")
|
||
if isinstance(xml, str):
|
||
result["data"]["xml"] = xml.replace("\x00", "").replace("\r", "")
|
||
return result
|