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

318 lines
9.2 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 - 设备管理路由
"""
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 AgentADB 直连也能用)
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", {})}