211 lines
6.8 KiB
Python
211 lines
6.8 KiB
Python
"""
|
|
语音命令路由
|
|
处理来自APP和PC端的语音指令
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List, Dict, Any
|
|
import json
|
|
import logging
|
|
import asyncio
|
|
|
|
from services.ai_agent import process_voice_command, ai_agent
|
|
from services.ws_hub import ws_hub
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/voice", tags=["语音控制"])
|
|
|
|
|
|
def _map_action_to_device(action: dict) -> dict:
|
|
"""将 AI 解析的 action 转为设备端 execute.data 格式"""
|
|
act = action.get("action", "")
|
|
params = action.get("params") or {}
|
|
if act == "open_app":
|
|
return {"action": "app_start", "params": {"package": params.get("package", "")}}
|
|
if act == "input_text":
|
|
return {"action": "input", "params": {"text": params.get("text", ""), "clear": False}}
|
|
if act == "key_event":
|
|
keycode = (params.get("keycode") or "KEYCODE_BACK").lower()
|
|
key = "back" if "back" in keycode else ("home" if "home" in keycode else "back")
|
|
return {"action": "press_key", "params": {"key": key}}
|
|
if act == "back":
|
|
return {"action": "press_key", "params": {"key": "back"}}
|
|
if act == "home":
|
|
return {"action": "press_key", "params": {"key": "home"}}
|
|
if act == "screenshot":
|
|
return {"action": "screenshot", "params": params}
|
|
return {"action": act, "params": params}
|
|
|
|
|
|
class VoiceCommandRequest(BaseModel):
|
|
"""语音命令请求"""
|
|
text: str
|
|
device_id: Optional[str] = None
|
|
project_id: Optional[str] = None
|
|
execute: bool = True # 是否立即执行
|
|
|
|
|
|
class VoiceCommandResponse(BaseModel):
|
|
"""语音命令响应"""
|
|
success: bool
|
|
message: str
|
|
text: str
|
|
actions: List[Dict[str, Any]]
|
|
executed: bool = False
|
|
|
|
|
|
@router.post("/command", response_model=VoiceCommandResponse)
|
|
async def voice_command(request: VoiceCommandRequest):
|
|
"""
|
|
处理语音命令
|
|
|
|
接收文本命令,解析意图,可选择立即执行
|
|
|
|
示例:
|
|
POST /api/v3/voice/command
|
|
{
|
|
"text": "打开微信",
|
|
"device_id": "device_001",
|
|
"execute": true
|
|
}
|
|
"""
|
|
# 解析命令
|
|
result = await process_voice_command(request.text, request.device_id)
|
|
|
|
executed = False
|
|
|
|
if request.execute and result["success"] and request.device_id:
|
|
try:
|
|
for action in result["actions"]:
|
|
payload = _map_action_to_device(action)
|
|
command = {
|
|
"type": "execute",
|
|
"command_id": f"voice_{id(action)}",
|
|
"data": payload
|
|
}
|
|
await ws_hub.send_to_device(request.device_id, command)
|
|
if action.get("action") == "wait":
|
|
wait_time = action.get("params", {}).get("seconds", 1)
|
|
await asyncio.sleep(min(max(wait_time, 0.5), 10))
|
|
executed = True
|
|
except Exception as e:
|
|
logger.error(f"执行命令失败: {e}")
|
|
result["message"] += f" (执行失败: {str(e)})"
|
|
|
|
return VoiceCommandResponse(
|
|
success=result["success"],
|
|
message=result["message"],
|
|
text=result["text"],
|
|
actions=result["actions"],
|
|
executed=executed
|
|
)
|
|
|
|
|
|
@router.post("/parse")
|
|
async def parse_command(request: VoiceCommandRequest):
|
|
"""
|
|
仅解析命令,不执行
|
|
|
|
用于预览AI解析结果
|
|
"""
|
|
result = await process_voice_command(request.text, request.device_id)
|
|
return result
|
|
|
|
|
|
@router.websocket("/ws")
|
|
async def voice_websocket(websocket: WebSocket):
|
|
"""
|
|
语音命令WebSocket
|
|
|
|
用于PC端实时语音控制
|
|
|
|
消息格式:
|
|
发送: {"text": "打开微信", "device_id": "xxx"}
|
|
接收: {"success": true, "actions": [...], "executed": true}
|
|
"""
|
|
await websocket.accept()
|
|
logger.info("PC语音控制连接已建立")
|
|
|
|
try:
|
|
while True:
|
|
# 接收消息
|
|
data = await websocket.receive_text()
|
|
message = json.loads(data)
|
|
|
|
text = message.get("text", "")
|
|
device_id = message.get("device_id")
|
|
execute = message.get("execute", True)
|
|
|
|
if not text:
|
|
await websocket.send_json({
|
|
"success": False,
|
|
"message": "请提供语音文本"
|
|
})
|
|
continue
|
|
|
|
# 处理命令
|
|
result = await process_voice_command(text, device_id)
|
|
|
|
executed = False
|
|
|
|
if execute and result["success"] and device_id:
|
|
try:
|
|
for action in result["actions"]:
|
|
payload = _map_action_to_device(action)
|
|
command = {
|
|
"type": "execute",
|
|
"command_id": f"voice_ws_{id(action)}",
|
|
"data": payload
|
|
}
|
|
await ws_hub.send_to_device(device_id, command)
|
|
if action.get("action") == "wait":
|
|
wait_time = action.get("params", {}).get("seconds", 1)
|
|
await asyncio.sleep(min(max(wait_time, 0.5), 10))
|
|
executed = True
|
|
except Exception as e:
|
|
logger.error(f"执行失败: {e}")
|
|
result["message"] += f" (执行失败: {str(e)})"
|
|
|
|
# 返回结果
|
|
result["executed"] = executed
|
|
await websocket.send_json(result)
|
|
|
|
except WebSocketDisconnect:
|
|
logger.info("PC语音控制连接断开")
|
|
except Exception as e:
|
|
logger.error(f"WebSocket错误: {e}")
|
|
|
|
|
|
@router.get("/apps")
|
|
async def get_common_apps():
|
|
"""
|
|
获取常用应用列表
|
|
|
|
用于语音识别提示
|
|
"""
|
|
return {
|
|
"apps": [
|
|
{"name": "微信", "package": "com.tencent.mm"},
|
|
{"name": "抖音", "package": "com.ss.android.ugc.aweme"},
|
|
{"name": "支付宝", "package": "com.eg.android.AlipayGphone"},
|
|
{"name": "淘宝", "package": "com.taobao.taobao"},
|
|
{"name": "微博", "package": "com.sina.weibo"},
|
|
{"name": "QQ", "package": "com.tencent.mobileqq"},
|
|
{"name": "设置", "package": "com.android.settings"},
|
|
{"name": "相机", "package": "com.android.camera"},
|
|
{"name": "浏览器", "package": "com.android.chrome"},
|
|
],
|
|
"commands": [
|
|
"打开微信",
|
|
"返回",
|
|
"回到桌面",
|
|
"向上滑动",
|
|
"向下滑动",
|
|
"截图",
|
|
"发送消息给xxx",
|
|
"搜索xxx",
|
|
]
|
|
}
|