318 lines
9.2 KiB
Python
318 lines
9.2 KiB
Python
"""
|
||
工作手机SDK v3.0 - 设备管理路由
|
||
"""
|
||
|
||
from fastapi import APIRouter, HTTPException, Depends
|
||
from typing import List, Optional
|
||
from pydantic import BaseModel
|
||
|
||
from services.ws_hub import ws_hub
|
||
from services.device_manager import device_manager
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
# ========== 数据模型 ==========
|
||
|
||
class DeviceResponse(BaseModel):
|
||
"""设备信息响应"""
|
||
device_id: str
|
||
name: Optional[str] = None
|
||
model: Optional[str] = None
|
||
status: str = "offline"
|
||
android_version: Optional[str] = None
|
||
agent_version: Optional[str] = None
|
||
capabilities: List[str] = []
|
||
apps: List[str] = []
|
||
last_heartbeat: Optional[str] = None
|
||
|
||
|
||
class ScreenshotResponse(BaseModel):
|
||
"""截图响应"""
|
||
image_url: Optional[str] = None
|
||
base64: Optional[str] = None
|
||
width: int
|
||
height: int
|
||
|
||
|
||
class ClickRequest(BaseModel):
|
||
"""点击请求"""
|
||
x: int
|
||
y: int
|
||
|
||
|
||
class ClickTextRequest(BaseModel):
|
||
"""点击文字请求"""
|
||
text: str
|
||
timeout: int = 10
|
||
|
||
|
||
class InputRequest(BaseModel):
|
||
"""输入请求"""
|
||
text: str
|
||
clear: bool = True
|
||
|
||
|
||
class SwipeRequest(BaseModel):
|
||
"""滑动请求"""
|
||
direction: str # up, down, left, right
|
||
scale: float = 0.8
|
||
|
||
|
||
class HeartbeatConfigRequest(BaseModel):
|
||
"""心跳配置请求"""
|
||
heartbeat_interval_seconds: int
|
||
|
||
|
||
# ========== 设备列表 ==========
|
||
|
||
@router.get("/devices", response_model=dict)
|
||
async def get_devices():
|
||
"""获取所有设备列表(融合 WebSocket + ADB + DB)"""
|
||
from services.adb_device import adb_manager
|
||
|
||
# 1. 尝试从数据库获取设备(容错:MongoDB 挂了不影响)
|
||
db_devices = []
|
||
try:
|
||
db_devices = await device_manager.get_all_devices()
|
||
except Exception as e:
|
||
import logging
|
||
logging.getLogger(__name__).warning(f"MongoDB 查询失败(已跳过): {e}")
|
||
|
||
# 2. 合并 WebSocket 在线状态
|
||
online_devices = {d["device_id"]: d for d in ws_hub.get_online_devices()}
|
||
|
||
seen_ids = set()
|
||
devices = []
|
||
|
||
for device in db_devices:
|
||
device_id = device["device_id"]
|
||
seen_ids.add(device_id)
|
||
if device_id in online_devices:
|
||
device.update(online_devices[device_id])
|
||
device["status"] = "online"
|
||
else:
|
||
device["status"] = "offline"
|
||
devices.append(device)
|
||
|
||
# 添加只在 WebSocket 但不在数据库的设备
|
||
for device_id, info in online_devices.items():
|
||
if device_id not in seen_ids:
|
||
seen_ids.add(device_id)
|
||
devices.append(info)
|
||
|
||
# 3. 融合 ADB 设备(关键:即使没有 WebSocket Agent,ADB 直连也能用)
|
||
adb_serials = adb_manager.scan_devices()
|
||
for serial in adb_serials:
|
||
if serial not in seen_ids:
|
||
adb_dev = adb_manager.get_device(serial)
|
||
if adb_dev:
|
||
info = adb_dev.get_info()
|
||
devices.append({
|
||
"device_id": serial,
|
||
"model": info.get("model", "Unknown"),
|
||
"brand": info.get("brand", "Unknown"),
|
||
"android_version": info.get("android_version", "Unknown"),
|
||
"status": "adb", # ADB 直连模式
|
||
"connection_type": "adb",
|
||
"display": info.get("display", {}),
|
||
})
|
||
|
||
return {"code": 200, "data": devices}
|
||
|
||
|
||
@router.get("/devices/{device_id}", response_model=dict)
|
||
async def get_device(device_id: str):
|
||
"""获取设备详情(支持 WebSocket / ADB / DB)"""
|
||
from services.adb_device import adb_manager
|
||
|
||
# 检查 WebSocket 在线
|
||
online_info = ws_hub.get_device_info(device_id)
|
||
|
||
# 检查数据库(容错)
|
||
db_info = None
|
||
try:
|
||
db_info = await device_manager.get_device(device_id)
|
||
except Exception:
|
||
pass
|
||
|
||
# 检查 ADB 直连
|
||
adb_info = None
|
||
adb_dev = adb_manager.get_device(device_id)
|
||
if adb_dev and adb_dev.is_online():
|
||
adb_info = adb_dev.get_info()
|
||
adb_info["connection_type"] = "adb"
|
||
|
||
if not online_info and not db_info and not adb_info:
|
||
raise HTTPException(status_code=404, detail="设备不存在")
|
||
|
||
device = {**(db_info or {}), **(adb_info or {}), **(online_info or {})}
|
||
if online_info:
|
||
device["status"] = "online"
|
||
elif adb_info:
|
||
device["status"] = "adb"
|
||
else:
|
||
device["status"] = "offline"
|
||
device["device_id"] = device_id
|
||
|
||
return {"code": 200, "data": device}
|
||
|
||
|
||
@router.get("/devices/{device_id}/heartbeat", response_model=dict)
|
||
async def get_device_heartbeat(device_id: str):
|
||
"""查询设备心跳状态"""
|
||
status = ws_hub.get_heartbeat_status(device_id)
|
||
if not status:
|
||
raise HTTPException(status_code=404, detail="设备不存在或未上报心跳")
|
||
return {"code": 200, "data": status}
|
||
|
||
|
||
@router.post("/devices/{device_id}/heartbeat/config", response_model=dict)
|
||
async def set_device_heartbeat(device_id: str, req: HeartbeatConfigRequest):
|
||
"""下发设备心跳配置(5-120 秒)"""
|
||
ok = await ws_hub.set_heartbeat_interval(device_id, req.heartbeat_interval_seconds)
|
||
if not ok:
|
||
raise HTTPException(status_code=400, detail="heartbeat_interval_seconds 必须在 5-120 之间")
|
||
return {
|
||
"code": 200,
|
||
"data": {
|
||
"device_id": device_id,
|
||
"heartbeat_interval_seconds": req.heartbeat_interval_seconds,
|
||
},
|
||
}
|
||
|
||
|
||
# ========== 设备控制 ==========
|
||
|
||
@router.post("/devices/{device_id}/screenshot", response_model=dict)
|
||
async def screenshot(device_id: str):
|
||
"""获取设备截图"""
|
||
|
||
if not ws_hub.is_online(device_id):
|
||
raise HTTPException(status_code=503, detail="设备不在线")
|
||
|
||
result = await ws_hub.send_command(device_id, {
|
||
"type": "execute",
|
||
"data": {
|
||
"action": "screenshot"
|
||
}
|
||
})
|
||
|
||
if result.get("code") != 200:
|
||
raise HTTPException(status_code=result.get("code", 500), detail=result.get("message"))
|
||
|
||
return {"code": 200, "data": result.get("data", {})}
|
||
|
||
|
||
@router.post("/devices/{device_id}/click", response_model=dict)
|
||
async def click(device_id: str, req: ClickRequest):
|
||
"""点击坐标"""
|
||
|
||
if not ws_hub.is_online(device_id):
|
||
raise HTTPException(status_code=503, detail="设备不在线")
|
||
|
||
result = await ws_hub.send_command(device_id, {
|
||
"type": "execute",
|
||
"data": {
|
||
"action": "click",
|
||
"params": {"x": req.x, "y": req.y}
|
||
}
|
||
})
|
||
|
||
return {"code": 200, "data": result.get("data", {})}
|
||
|
||
|
||
@router.post("/devices/{device_id}/click-text", response_model=dict)
|
||
async def click_text(device_id: str, req: ClickTextRequest):
|
||
"""点击文字"""
|
||
|
||
if not ws_hub.is_online(device_id):
|
||
raise HTTPException(status_code=503, detail="设备不在线")
|
||
|
||
result = await ws_hub.send_command(device_id, {
|
||
"type": "execute",
|
||
"data": {
|
||
"action": "click_text",
|
||
"params": {"text": req.text, "timeout": req.timeout}
|
||
}
|
||
})
|
||
|
||
return {"code": 200, "data": result.get("data", {})}
|
||
|
||
|
||
@router.post("/devices/{device_id}/input", response_model=dict)
|
||
async def input_text(device_id: str, req: InputRequest):
|
||
"""输入文字"""
|
||
|
||
if not ws_hub.is_online(device_id):
|
||
raise HTTPException(status_code=503, detail="设备不在线")
|
||
|
||
result = await ws_hub.send_command(device_id, {
|
||
"type": "execute",
|
||
"data": {
|
||
"action": "input",
|
||
"params": {"text": req.text, "clear": req.clear}
|
||
}
|
||
})
|
||
|
||
return {"code": 200, "data": result.get("data", {})}
|
||
|
||
|
||
@router.post("/devices/{device_id}/swipe", response_model=dict)
|
||
async def swipe(device_id: str, req: SwipeRequest):
|
||
"""滑动"""
|
||
|
||
if not ws_hub.is_online(device_id):
|
||
raise HTTPException(status_code=503, detail="设备不在线")
|
||
|
||
result = await ws_hub.send_command(device_id, {
|
||
"type": "execute",
|
||
"data": {
|
||
"action": "swipe",
|
||
"params": {"direction": req.direction, "scale": req.scale}
|
||
}
|
||
})
|
||
|
||
return {"code": 200, "data": result.get("data", {})}
|
||
|
||
|
||
@router.get("/devices/{device_id}/ui-tree", response_model=dict)
|
||
async def get_ui_tree(device_id: str):
|
||
"""获取UI树"""
|
||
|
||
if not ws_hub.is_online(device_id):
|
||
raise HTTPException(status_code=503, detail="设备不在线")
|
||
|
||
result = await ws_hub.send_command(device_id, {
|
||
"type": "execute",
|
||
"data": {
|
||
"action": "ui_tree"
|
||
}
|
||
})
|
||
|
||
return {"code": 200, "data": result.get("data", {})}
|
||
|
||
|
||
# ========== 脚本执行 ==========
|
||
|
||
@router.post("/devices/{device_id}/execute", response_model=dict)
|
||
async def execute_script(device_id: str, script: str, action: str, params: dict = {}, timeout: int = 30):
|
||
"""执行脚本"""
|
||
|
||
if not ws_hub.is_online(device_id):
|
||
raise HTTPException(status_code=503, detail="设备不在线")
|
||
|
||
result = await ws_hub.send_command(device_id, {
|
||
"type": "execute",
|
||
"data": {
|
||
"script": script,
|
||
"action": action,
|
||
"params": params
|
||
}
|
||
}, timeout=timeout)
|
||
|
||
# 记录日志
|
||
await device_manager.log_command(device_id, f"{script}.{action}", params, result)
|
||
|
||
return {"code": 200, "data": result.get("data", {})}
|