Files
Open-AutoGLM/dashboard/api/devices.py
let5sne.win10 3552df23d6 Add Web Dashboard with multi-device control and callback hooks
Features:
- Web Dashboard: FastAPI-based dashboard with Vue.js frontend
  - Multi-device support (ADB, HDC, iOS)
  - Real-time WebSocket updates for task progress
  - Device management with status tracking
  - Task queue with execution controls (start/stop/re-execute)
  - Detailed task information display (thinking, actions, completion messages)
  - Screenshot viewing per device
  - LAN deployment support with configurable CORS

- Callback Hooks: Interrupt and modify task execution
  - step_callback: Called after each step with StepResult
  - before_action_callback: Called before executing action
  - Support for task interruption and dynamic task switching
  - Example scripts demonstrating callback usage

- Configuration: Environment-based configuration
  - .env file support for all settings
  - .env.example template with documentation
  - Model API configuration (base URL, model name, API key)
  - Dashboard configuration (host, port, CORS, device type)
  - Phone agent configuration (delays, max steps, language)

Technical improvements:
- Fixed forward reference issue with StepResult
- Added package exports for callback types and configs
- Enhanced dependencies with FastAPI, WebSocket support
- Thread-safe task execution with device locking
- Async WebSocket broadcasting from sync thread pool

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-09 02:20:06 +08:00

194 lines
5.0 KiB
Python

"""
Device management API endpoints.
"""
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from dashboard.dependencies import get_device_manager, get_ws_manager
from dashboard.models.device import DeviceSchema, DeviceStatus
from dashboard.services.device_manager import DeviceManager
from dashboard.services.websocket_manager import WebSocketManager
router = APIRouter(prefix="/devices", tags=["devices"])
@router.get("", response_model=List[DeviceSchema])
async def list_devices(
device_manager: DeviceManager = Depends(get_device_manager),
):
"""List all connected devices.
Returns:
List of device schemas
"""
devices = await device_manager.refresh_devices()
return [
DeviceSchema(
device_id=d.device_id,
status=d.status,
device_type=d.device_type,
model=d.model,
android_version=d.android_version,
current_app=d.current_app,
last_seen=d.last_seen,
is_connected=d.is_connected,
)
for d in devices
]
@router.get("/refresh")
async def refresh_devices(
device_manager: DeviceManager = Depends(get_device_manager),
ws_manager: WebSocketManager = Depends(get_ws_manager),
):
"""Rescan for connected devices.
Returns:
Refresh confirmation
"""
devices = await device_manager.refresh_devices()
# Broadcast device update
for device in devices:
await ws_manager.broadcast_device_update(
device.device_id,
{
"status": device.status,
"is_connected": device.is_connected,
"model": device.model,
"current_app": device.current_app,
"last_seen": device.last_seen.isoformat(),
},
)
return {"message": "Devices refreshed", "count": len(devices)}
@router.get("/{device_id}", response_model=DeviceSchema)
async def get_device(
device_id: str,
device_manager: DeviceManager = Depends(get_device_manager),
):
"""Get device details.
Args:
device_id: Device identifier
Returns:
Device schema
Raises:
HTTPException: If device not found
"""
device = await device_manager.get_device(device_id)
if device is None:
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
return DeviceSchema(
device_id=device.device_id,
status=device.status,
device_type=device.device_type,
model=device.model,
android_version=device.android_version,
current_app=device.current_app,
last_seen=device.last_seen,
is_connected=device.is_connected,
)
@router.post("/{device_id}/connect")
async def connect_device(
device_id: str,
address: str | None = None,
device_manager: DeviceManager = Depends(get_device_manager),
):
"""Connect to device via WiFi.
Args:
device_id: Device identifier (for route matching)
address: Device address (IP:PORT)
Returns:
Connection result
"""
if not address:
raise HTTPException(status_code=400, detail="Address is required")
success = await device_manager.connect_device(address)
if success:
return {"message": f"Connected to {address}"}
else:
raise HTTPException(status_code=500, detail=f"Failed to connect to {address}")
@router.post("/{device_id}/disconnect")
async def disconnect_device(
device_id: str,
address: str | None = None,
device_manager: DeviceManager = Depends(get_device_manager),
):
"""Disconnect from device.
Args:
device_id: Device identifier (for route matching)
address: Device address (IP:PORT)
Returns:
Disconnection result
"""
if not address:
raise HTTPException(status_code=400, detail="Address is required")
success = await device_manager.disconnect_device(address)
if success:
return {"message": f"Disconnected from {address}"}
else:
raise HTTPException(
status_code=500, detail=f"Failed to disconnect from {address}"
)
@router.get("/{device_id}/screenshot")
async def get_device_screenshot(
device_id: str,
device_manager: DeviceManager = Depends(get_device_manager),
):
"""Get current device screenshot.
Args:
device_id: Device identifier
Returns:
Screenshot data (base64 encoded)
"""
screenshot = await device_manager.get_screenshot(device_id)
if screenshot is None:
raise HTTPException(status_code=500, detail="Failed to capture screenshot")
return {"device_id": device_id, "screenshot": screenshot}
@router.get("/{device_id}/current-app")
async def get_current_app(
device_id: str,
device_manager: DeviceManager = Depends(get_device_manager),
):
"""Get current app for device.
Args:
device_id: Device identifier
Returns:
Current app package name
"""
app = await device_manager.get_current_app(device_id)
return {"device_id": device_id, "current_app": app}