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

837 lines
31 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 - AI Agent路由
自然语言控制手机(支持 WebSocket Agent 和 ADB 直连双模式)
增强版:智能命令引擎,支持多步骤、坐标点击、复合命令
"""
from __future__ import annotations
from fastapi import APIRouter, HTTPException
from typing import Optional, List
from pydantic import BaseModel
import logging
import re
import time
import asyncio
from services.ws_hub import ws_hub
from services.device_manager import device_manager
from services.adb_device import adb_manager
from services.brain_skill_registry import SKILL_REGISTRY, registry_summary
from config import settings
router = APIRouter()
logger = logging.getLogger(__name__)
# ========== 常用 APP 包名映射(全量) ==========
APP_PACKAGES = {
# 社交
"微信": "com.tencent.mm", "wechat": "com.tencent.mm",
"QQ": "com.tencent.mobileqq", "qq": "com.tencent.mobileqq",
"飞书": "com.ss.android.lark", "lark": "com.ss.android.lark",
"钉钉": "com.alibaba.android.rimet",
"企业微信": "com.tencent.wework",
# 短视频/内容
"抖音": "com.ss.android.ugc.aweme", "douyin": "com.ss.android.ugc.aweme",
"小红书": "com.xingin.xhs", "xhs": "com.xingin.xhs",
"快手": "com.smile.gifmaker",
"B站": "tv.danmaku.bili", "bilibili": "tv.danmaku.bili",
# 电商
"闲鱼": "com.taobao.idlefish", "xianyu": "com.taobao.idlefish",
"淘宝": "com.taobao.taobao", "taobao": "com.taobao.taobao",
"京东": "com.jingdong.app.mall", "jd": "com.jingdong.app.mall",
"拼多多": "com.xunmeng.pinduoduo",
# 支付/金融
"支付宝": "com.eg.android.AlipayGphone", "alipay": "com.eg.android.AlipayGphone",
# 系统/工具
"设置": "com.android.settings", "settings": "com.android.settings",
"相机": "com.android.camera2", "camera": "com.android.camera2",
"浏览器": "com.android.chrome", "chrome": "com.android.chrome",
"Chrome": "com.android.chrome",
"电话": "com.google.android.dialer", "拨号": "com.google.android.dialer",
"短信": "com.google.android.apps.messaging", "消息": "com.google.android.apps.messaging",
"相册": "com.google.android.apps.photos", "photos": "com.google.android.apps.photos",
"文件管理": "com.android.documentsui",
"日历": "com.google.android.calendar",
"计算器": "com.google.android.calculator",
"时钟": "com.google.android.deskclock",
"地图": "com.google.android.apps.maps", "maps": "com.google.android.apps.maps",
# 终端
"termux": "com.termux", "Termux": "com.termux", "终端": "com.termux",
# 外卖/生活
"美团": "com.sankuai.meituan",
"饿了么": "me.ele",
"高德": "com.autonavi.minimap", "高德地图": "com.autonavi.minimap",
"滴滴": "com.sdu.didi.psnger",
}
# ========== 数据模型 ==========
class ExecuteTaskRequest(BaseModel):
"""执行任务请求"""
device_id: str
task: str
llm_provider: str = "configured" # 统一 AI 配置默认龙虾U盘/OpenAI兼容
max_steps: int = 30
class ExecuteTaskResponse(BaseModel):
"""执行任务响应"""
success: bool
steps: List[str] = []
duration_ms: int = 0
error: Optional[str] = None
# ========== 智能命令引擎 ==========
def _execute_single(adb_dev, cmd: str) -> dict:
"""执行单条解析后的命令,返回 {success, step, detail}"""
cmd = cmd.strip()
if not cmd:
return {"success": True, "step": "空命令跳过"}
# --- 打开 APP ---
for name, pkg in APP_PACKAGES.items():
if cmd == name or cmd == f"打开{name}" or cmd.lower() == f"open {name.lower()}":
r = adb_dev.start_app(pkg)
return {"success": r.get("code") == 200, "step": f"打开 {name}", "detail": r}
# 通过包名打开
pkg_match = re.match(r"^(?:打开|启动|open)\s*(com\.\S+)", cmd)
if pkg_match:
pkg = pkg_match.group(1)
r = adb_dev.start_app(pkg)
return {"success": r.get("code") == 200, "step": f"打开 {pkg}", "detail": r}
# --- 按键 ---
if cmd in ("主页", "回主页", "回首页", "home", "Home", "HOME"):
r = adb_dev.press_key("home")
return {"success": r.get("code") == 200, "step": "Home", "detail": r}
if cmd in ("返回", "back", "Back", "BACK"):
r = adb_dev.press_key("back")
return {"success": r.get("code") == 200, "step": "返回", "detail": r}
if cmd in ("最近任务", "多任务", "recent", "Recent"):
r = adb_dev.press_key("recent")
return {"success": r.get("code") == 200, "step": "最近任务", "detail": r}
if cmd in ("回车", "确认", "enter", "Enter"):
r = adb_dev.press_key("enter")
return {"success": r.get("code") == 200, "step": "回车", "detail": r}
if cmd in ("音量加", "音量+", "volume_up"):
r = adb_dev.press_key("volume_up")
return {"success": r.get("code") == 200, "step": "音量+", "detail": r}
if cmd in ("音量减", "音量-", "volume_down"):
r = adb_dev.press_key("volume_down")
return {"success": r.get("code") == 200, "step": "音量-", "detail": r}
if cmd in ("锁屏", "电源", "power"):
r = adb_dev.press_key("KEYCODE_POWER")
return {"success": r.get("code") == 200, "step": "电源键", "detail": r}
# --- 截图 ---
if cmd in ("截图", "截屏", "screenshot"):
r = adb_dev.screenshot()
return {"success": r.get("code") == 200, "step": "截图", "detail": r}
# --- 滑动 ---
swipe_map = {
"上滑": "up", "往上滑": "up", "向上滑": "up", "翻页": "up", "下一页": "up",
"下滑": "down", "往下滑": "down", "向下滑": "down", "刷新": "down", "上一页": "down",
"左滑": "left", "往左滑": "left", "向左滑": "left",
"右滑": "right", "往右滑": "right", "向右滑": "right",
}
if cmd in swipe_map:
r = adb_dev.swipe(swipe_map[cmd])
return {"success": r.get("code") == 200, "step": cmd, "detail": r}
# --- 坐标点击: "点击 540 1200" 或 "tap 540 1200" ---
coord_match = re.match(r"(?:点击|tap|click)\s*(\d+)[,\s]+(\d+)", cmd)
if coord_match:
x, y = int(coord_match.group(1)), int(coord_match.group(2))
r = adb_dev.click(x, y)
return {"success": r.get("code") == 200, "step": f"点击坐标 ({x},{y})", "detail": r}
# --- 纯坐标 "540,1200" 或 "540 1200" ---
pure_coord = re.match(r"^(\d{2,4})[,\s]+(\d{2,4})$", cmd)
if pure_coord:
x, y = int(pure_coord.group(1)), int(pure_coord.group(2))
r = adb_dev.click(x, y)
return {"success": r.get("code") == 200, "step": f"点击坐标 ({x},{y})", "detail": r}
# --- 点击文字 ---
click_match = re.match(r"(?:点击|点|按|选择|找到|tap|click)\s*[「\"']?(.+?)[」\"']?\s*$", cmd)
if click_match:
text = click_match.group(1).strip()
r = adb_dev.click_text(text)
return {"success": r.get("code") == 200, "step": f"点击「{text}", "detail": r}
# --- 输入文字 ---
input_match = re.match(r"(?:输入|打字|写|type|input|write)\s*[:\s]*(.+)", cmd)
if input_match:
text = input_match.group(1).strip()
r = adb_dev.input_text(text)
return {"success": r.get("code") == 200, "step": f"输入「{text}", "detail": r}
# --- 等待 ---
wait_match = re.match(r"(?:等待|等|wait|sleep)\s*(\d+)\s*(?:秒|s)?", cmd)
if wait_match:
secs = min(int(wait_match.group(1)), 30) # 最多30秒
time.sleep(secs)
return {"success": True, "step": f"等待 {secs}"}
# --- 查看当前APP ---
if cmd in ("当前APP", "当前应用", "current_app", "current"):
r = adb_dev.current_app()
return {"success": r.get("code") == 200, "step": "查看当前APP", "detail": r}
# --- 已安装APP ---
if cmd in ("已安装", "装了什么", "app列表", "list_apps"):
r = adb_dev.installed_apps()
return {"success": r.get("code") == 200, "step": "查看已安装APP", "detail": r}
# --- UI树 ---
if cmd in ("ui树", "UI树", "ui_tree", "界面分析"):
r = adb_dev.get_ui_tree()
return {"success": r.get("code") == 200, "step": "获取UI树", "detail": r}
# --- 关闭APP ---
close_match = re.match(r"(?:关闭|停止|杀掉|kill|stop)\s*(.+)", cmd)
if close_match:
app_name = close_match.group(1).strip()
pkg = APP_PACKAGES.get(app_name, app_name)
r = adb_dev.stop_app(pkg)
return {"success": r.get("code") == 200, "step": f"关闭 {app_name}", "detail": r}
return None # 未匹配
def _execute_ai_action(adb_dev, action: dict) -> dict:
"""执行 AI 返回的单个操作"""
act = action.get("action", "")
steps = []
if act == "open_app":
app_name = action.get("app", "")
pkg = APP_PACKAGES.get(app_name, app_name)
# 如果还不是包名格式,尝试模糊匹配
if "." not in pkg:
for name, p in APP_PACKAGES.items():
if name.lower() == app_name.lower() or app_name.lower() in name.lower():
pkg = p
break
r = adb_dev.start_app(pkg)
return {"success": r.get("code") == 200, "step": f"打开 {app_name}", "detail": r}
elif act == "click":
if "text" in action:
r = adb_dev.click_text(action["text"])
return {"success": r.get("code") == 200, "step": f"点击「{action['text']}", "detail": r}
elif "x" in action and "y" in action:
r = adb_dev.click(int(action["x"]), int(action["y"]))
return {"success": r.get("code") == 200, "step": f"点击 ({action['x']},{action['y']})", "detail": r}
elif act == "input":
text = action.get("text", "")
r = adb_dev.input_text(text)
return {"success": r.get("code") == 200, "step": f"输入「{text}", "detail": r}
elif act == "back":
r = adb_dev.press_key("back")
return {"success": r.get("code") == 200, "step": "返回", "detail": r}
elif act == "home":
r = adb_dev.press_key("home")
return {"success": r.get("code") == 200, "step": "主页", "detail": r}
elif act == "swipe":
direction = action.get("direction", "up")
r = adb_dev.swipe(direction)
return {"success": r.get("code") == 200, "step": f"滑动{direction}", "detail": r}
elif act == "wait":
secs = min(int(action.get("seconds", 1)), 3) # AI 等待最多3秒
time.sleep(secs)
return {"success": True, "step": f"等待 {secs}"}
elif act == "screenshot":
r = adb_dev.screenshot()
return {"success": r.get("code") == 200, "step": "截图", "detail": r}
elif act == "key_event":
key = action.get("key", "")
key_map = {
"enter": "enter", "回车": "enter",
"back": "back", "返回": "back",
"home": "home", "主页": "home",
"tab": "tab",
"delete": "del", "删除": "del",
"search": "search", "搜索": "search",
}
key_name = key_map.get(key.lower(), key)
r = adb_dev.press_key(key_name)
return {"success": r.get("code") == 200, "step": f"按键 {key}", "detail": r}
elif act == "close_app":
app_name = action.get("app", "")
pkg = APP_PACKAGES.get(app_name, app_name)
r = adb_dev.stop_app(pkg)
return {"success": r.get("code") == 200, "step": f"关闭 {app_name}", "detail": r}
elif act == "shell":
# 执行 ADB shell 命令(用于系统设置等高级操作)
command = action.get("command", "")
if not command:
return {"success": False, "step": "shell 命令为空"}
# 安全检查:禁止危险命令
dangerous = ["rm -rf /", "format", "wipe", "factory_reset", "reboot"]
if any(d in command.lower() for d in dangerous):
return {"success": False, "step": f"危险命令被阻止: {command}"}
try:
r = adb_dev._shell(command, timeout=10)
return {"success": True, "step": f"执行: {command}", "detail": {"output": r}}
except Exception as e:
return {"success": False, "step": f"shell 失败: {command}", "detail": {"error": str(e)}}
# 未知操作:尝试智能回退
logger.warning(f"[AI] 未知操作类型: {act},尝试智能回退")
# 回退策略1如果有 app 字段,当作 open_app
if "app" in action:
app_name = action["app"]
pkg = APP_PACKAGES.get(app_name, app_name)
if "." not in pkg:
for name, p in APP_PACKAGES.items():
if name.lower() == app_name.lower():
pkg = p
break
r = adb_dev.start_app(pkg)
return {"success": r.get("code") == 200, "step": f"打开 {app_name} (回退)", "detail": r}
# 回退策略2如果有 text 字段,当作 click
if "text" in action:
r = adb_dev.click_text(action["text"])
return {"success": r.get("code") == 200, "step": f"点击「{action['text']}」(回退)", "detail": r}
# 回退策略3如果有 command 字段,当作 shell
if "command" in action:
try:
r = adb_dev._shell(action["command"], timeout=10)
return {"success": True, "step": f"执行: {action['command']} (回退)"}
except Exception:
pass
return {"success": False, "step": f"未知操作: {act}"}
def _parse_and_execute_adb(device_id: str, task: str) -> dict:
"""
智能命令引擎:解析自然语言,支持单步和多步命令。
"然后" / "" / ";" 分隔多步骤。
如果无法精确匹配,尝试智能推断。
"""
adb_dev = adb_manager.get_device(device_id)
if not adb_dev:
return {"success": False, "error": f"ADB 设备 {device_id} 不可用"}
task = task.strip()
if not task:
return {"success": False, "error": "命令为空"}
# 分割多步命令(支持 "然后"、""、""、";"、"→"、"->"
parts = re.split(r"[;]\s*|然后\s*|之后\s*|→\s*|->\s*", task)
# 如果只有一段且包含逗号分隔的完整子命令,也拆分
if len(parts) == 1 and "" in task:
# 只在确实是多个命令时才按逗号拆(不拆坐标等)
potential = re.split(r"[]\s*", task)
if len(potential) > 1 and all(len(p.strip()) > 1 for p in potential):
parts = potential
all_steps = []
all_success = True
last_detail = None
needs_ai_parts = []
for part in parts:
part = part.strip()
if not part:
continue
result = _execute_single(adb_dev, part)
if result is None:
# 智能推断:尝试作为 APP 名打开
for name, pkg in APP_PACKAGES.items():
if name.lower() == part.lower() or name == part:
r = adb_dev.start_app(pkg)
result = {"success": r.get("code") == 200, "step": f"打开 {name}", "detail": r}
break
if result is None:
# 智能推断:纯数字当成等待秒数(<=10或当成文字点击
if part.isdigit():
num = int(part)
if num <= 10:
time.sleep(num)
result = {"success": True, "step": f"等待 {num}"}
else:
# 当作文字点击
r = adb_dev.click_text(part)
result = {"success": r.get("code") == 200, "step": f"点击「{part}", "detail": r}
if result is None:
# 最终尝试当作要点击的文字UI上找这个文字
r = adb_dev.click_text(part)
if r.get("code") == 200:
result = {"success": True, "step": f"点击「{part}", "detail": r}
else:
# 标记为需要 AI 处理
result = {"success": False, "step": part, "needs_ai": True}
all_steps.append(result.get("step", part))
last_detail = result.get("detail")
if not result.get("success"):
if result.get("needs_ai"):
# 收集需要 AI 处理的部分
needs_ai_parts.append(part)
else:
all_success = False
if result.get("error"):
all_steps[-1] += f" (失败: {result['error']})"
# 多步之间短暂等待
if len(parts) > 1:
time.sleep(0.5)
# 如果有需要 AI 处理的部分,在最后返回时标记
if needs_ai_parts:
return {
"success": all_success if not needs_ai_parts else None, # None = 需要 AI
"steps": all_steps,
"detail": last_detail,
"total_steps": len(all_steps),
"needs_ai": True,
"ai_query": task, # 把原始完整命令传给 AI
}
return {
"success": all_success,
"steps": all_steps,
"detail": last_detail,
"total_steps": len(all_steps),
}
# ========== AI Agent接口 ==========
@router.post("/agent/execute", response_model=dict)
async def execute_task(req: ExecuteTaskRequest):
"""
执行自然语言任务(自动选择 WebSocket Agent 或 ADB 直连模式)
示例任务:
- "打开微信"
- "回主页"
- "点击 发现"
- "输入 你好"
"""
# 优先使用 WebSocket Agent功能更强
if ws_hub.is_online(req.device_id):
from services.ai_provider import get_config
ai_config = get_config()
if req.llm_provider == "configured" and not ai_config.get("api_key"):
raise HTTPException(status_code=400, detail="统一 AI 接口尚未配置 API Key")
result = await ws_hub.send_command(req.device_id, {
"type": "agent_execute",
"data": {
"task": req.task,
"llm_provider": req.llm_provider,
"max_steps": req.max_steps
}
}, timeout=120)
if result.get("code") != 200:
return {
"code": result.get("code", 500),
"data": {"success": False, "error": result.get("message", "执行失败")}
}
try:
await device_manager.log_command(req.device_id, "agent.execute", {"task": req.task}, result)
except Exception:
pass
return {"code": 200, "data": result.get("data", {})}
# 降级ADB 本地直连执行
adb_dev = adb_manager.get_device(req.device_id)
if adb_dev and adb_dev.is_online():
logger.info(f"[agent] ADB 直连执行: {req.device_id} -> {req.task}")
result = _parse_and_execute_adb(req.device_id, req.task)
# 如果模式匹配失败,调用 AI 引擎
if result.get("needs_ai"):
logger.info(f"[agent] 模式匹配失败,调用 AI: {req.task}")
from services.ai_command import ai_engine
# 获取当前屏幕上下文
context = None
try:
app_info = adb_dev.current_app()
if app_info.get("code") == 200:
context = {"current_app": app_info["data"].get("package", "unknown")}
except Exception:
pass
ai_actions = await ai_engine.parse(req.task, context)
if ai_actions:
ai_steps = []
ai_success = True
for action in ai_actions:
r = _execute_ai_action(adb_dev, action)
ai_steps.append(r.get("step", str(action)))
if not r.get("success"):
ai_success = False
time.sleep(0.5)
return {"code": 200, "data": {
"success": ai_success,
"steps": ai_steps,
"total_steps": len(ai_steps),
"engine": "ai",
}}
else:
# AI 也失败了
return {"code": 200, "data": {
"success": False,
"error": f"无法理解命令: {req.task}",
"hint": "AI 引擎无法解析。试试: 打开微信/点击XXX/输入XXX/截图 等",
}}
return {"code": 200, "data": result}
raise HTTPException(status_code=503, detail="设备不在线WebSocket 和 ADB 均不可用)")
@router.get("/agent/status/{device_id}", response_model=dict)
async def get_agent_status(device_id: str):
"""获取Agent状态"""
if not ws_hub.is_online(device_id):
return {
"code": 200,
"data": {
"status": "offline",
"device_id": device_id
}
}
result = await ws_hub.send_command(device_id, {
"type": "agent_status"
}, timeout=5)
return {
"code": 200,
"data": result.get("data", {"status": "ready"})
}
@router.post("/agent/stop/{device_id}", response_model=dict)
async def stop_agent(device_id: str):
"""停止正在执行的Agent任务"""
if not ws_hub.is_online(device_id):
raise HTTPException(status_code=503, detail="设备不在线")
result = await ws_hub.send_command(device_id, {
"type": "agent_stop"
}, timeout=5)
return {
"code": 200,
"data": {"stopped": True}
}
# ========== AI 引擎端点 ==========
@router.get("/ai/status", response_model=dict)
async def ai_status():
"""获取 AI 引擎状态"""
from services.ai_command import ai_engine
status = await ai_engine.get_status()
return {"code": 200, "data": status}
@router.get("/ai/config", response_model=dict)
async def get_ai_config():
"""读取统一 AI 接口配置(密钥仅返回已配置状态)。"""
from services.ai_provider import public_config
return {"code": 200, "data": public_config()}
class AIConfigRequest(BaseModel):
provider: Optional[str] = None
base_url: Optional[str] = None
api_key: Optional[str] = None
model: Optional[str] = None
enabled: Optional[bool] = None
@router.post("/ai/config", response_model=dict)
async def save_ai_config(req: AIConfigRequest):
"""保存统一 AI 接口配置,供 Agent 与设备管理共同使用。"""
from services.ai_provider import save_config
try:
data = save_config(req.model_dump(exclude_none=True))
return {"code": 200, "data": data, "message": "统一 AI 接口已保存"}
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
class AIChatRequest(BaseModel):
message: str
device_id: Optional[str] = None
@router.post("/ai/chat", response_model=dict)
async def ai_chat(req: AIChatRequest):
"""
AI 对话控制手机 —— 核心端点
用户输入自然语言AI 返回并执行操作
"""
from services.ai_command import ai_engine
t0 = time.time()
# 1. 获取设备(跳过 current_app 避免 3 秒开销AI 不需要这个上下文)
context = None
adb_dev = None
if req.device_id:
adb_dev = adb_manager.get_device(req.device_id)
t1 = time.time()
logger.info(f"[ai_chat] 设备获取: {t1-t0:.2f}s")
# 2. AI 解析
actions = await ai_engine.parse(req.message, context)
t2 = time.time()
logger.info(f"[ai_chat] AI 解析: {t2-t1:.2f}s ({len(actions)} 步)")
if not actions:
return {"code": 200, "data": {
"success": False,
"message": f"AI 无法解析: {req.message}",
"actions": [],
}}
# 3. 如果有设备,自动执行(放到线程池避免阻塞事件循环)
executed_steps = []
if adb_dev:
def _run_actions():
results = []
for i, action in enumerate(actions):
ta = time.time()
try:
r = _execute_ai_action(adb_dev, action)
results.append(r)
tb = time.time()
logger.info(f"[ai_chat] 步骤{i+1} {action.get('action')}: {tb-ta:.2f}s {'' if r.get('success') else ''}")
# wait 动作已自带延时,其他操作短暂间隔
if action.get("action") != "wait":
time.sleep(0.2)
except Exception as e:
results.append({"success": False, "step": f"异常: {e}"})
logger.error(f"[ai_chat] 步骤{i+1} 异常: {e}")
return results
try:
executed_steps = await asyncio.wait_for(
asyncio.to_thread(_run_actions),
timeout=20.0 # 总执行时间上限
)
except asyncio.TimeoutError:
executed_steps.append({"success": False, "step": "执行超时20秒上限"})
logger.warning(f"[ai_chat] 执行超时!")
t3 = time.time()
logger.info(f"[ai_chat] 总耗时: {t3-t0:.2f}s")
return {"code": 200, "data": {
"success": all(s.get("success") for s in executed_steps) if executed_steps else True,
"message": f"AI 解析 {len(actions)}" + (f",已执行 {len(executed_steps)}" if executed_steps else ""),
"actions": actions,
"executed": executed_steps,
"engine": "ai",
}}
# ========== AI Brain 技能注册表 ==========
# 真源: sdk/app/services/brain_skill_registry.py
@router.get("/ai/brain/skill-registry")
async def get_skill_registry():
"""AI Brain 技能注册表 — 返回所有可用技能、模块、操作的完整清单"""
return {
"code": 200,
"data": {
"skills": SKILL_REGISTRY,
"summary": registry_summary(),
},
}
@router.get("/ai/brain/dashboard")
async def ai_brain_dashboard():
"""AI Brain 仪表盘 — 汇总 AI Brain 在全部设备上的状态"""
online_devices = ws_hub.get_online_devices() if hasattr(ws_hub, 'get_online_devices') else []
try:
all_devices = await device_manager.get_all_devices() if hasattr(device_manager, 'get_all_devices') else []
except Exception:
all_devices = []
try:
adb_count = len(adb_manager.scan_devices()) if hasattr(adb_manager, 'scan_devices') else 0
except Exception:
adb_count = 0
total_skills = len(SKILL_REGISTRY)
total_actions = sum(len(a) for s in SKILL_REGISTRY.values() for a in s["modules"].values())
channels = {
"frida_hook": {"name": "Frida Hook", "priority": 1, "desc": "直接调用 APP 内部方法50-200ms"},
"u2_automation": {"name": "uiautomator2", "priority": 2, "desc": "UI 自动化模拟操作2-15s"},
"adb_shell": {"name": "ADB Shell", "priority": 3, "desc": "ADB 命令控制1-10s"},
"ai_agent": {"name": "AI Agent", "priority": 4, "desc": "LLM 意图解析 + 步骤执行"},
}
return {
"code": 200,
"data": {
"devices": {
"ws_online": len(online_devices) if isinstance(online_devices, list) else online_devices,
"adb_online": adb_count,
"total_registered": len(all_devices) if isinstance(all_devices, list) else all_devices,
},
"skills": {"total_skills": total_skills, "total_actions": total_actions},
"channels": channels,
"ai_brain": {
"version": "1.0.0",
"architecture": "v3.1 Frida优先 → u2兜底",
"heartbeat_interval": 60,
"features": [
"心跳驱动自主决策",
"离线自主运行模式",
"Frida 优先通道",
"任务队列管理",
"常驻指令执行",
"离线缓冲上传",
],
},
},
}
@router.post("/ai/brain/execute-script")
async def execute_brain_script(request: dict):
"""
AI Brain 脚本执行器 — 向指定设备发送技能脚本
Body: {device_id, script, action, params, channel?}
"""
device_id = request.get("device_id")
script = request.get("script")
action = request.get("action")
params = request.get("params", {})
channel = request.get("channel", "auto")
if not device_id or not script or not action:
raise HTTPException(status_code=400, detail="device_id, script, action 必填")
if ws_hub.is_online(device_id):
cmd = {
"type": "execute",
"script": script,
"action": action,
"params": params,
"channel": channel,
}
result = await ws_hub.send_command(device_id, cmd, timeout=60)
return {"code": 200, "data": result}
adb_dev = adb_manager.get_device(device_id)
if adb_dev and adb_dev.is_online():
raise HTTPException(status_code=501, detail="ADB 模式暂不支持直接技能执行,请通过 /agent/execute 调用")
raise HTTPException(status_code=503, detail=f"设备 {device_id} 不在线")
@router.post("/ai/brain/batch-execute")
async def batch_execute_script(request: dict):
"""
AI Brain 批量脚本执行 — 向多台设备同时发送相同脚本
Body: {device_ids: [...], script, action, params}
"""
device_ids = request.get("device_ids", [])
script = request.get("script")
action = request.get("action")
params = request.get("params", {})
if not device_ids or not script or not action:
raise HTTPException(status_code=400, detail="device_ids, script, action 必填")
results = {}
tasks = []
for did in device_ids:
if ws_hub.is_online(did):
cmd = {"type": "execute", "script": script, "action": action, "params": params}
tasks.append((did, ws_hub.send_command(did, cmd, timeout=60)))
else:
results[did] = {"code": 503, "message": "设备不在线"}
for did, task in tasks:
try:
result = await task
results[did] = result
except Exception as e:
results[did] = {"code": 500, "message": str(e)}
return {
"code": 200,
"data": {
"results": results,
"total": len(device_ids),
"success": sum(1 for r in results.values() if isinstance(r, dict) and r.get("code") == 200),
},
}
# ========== OpenAI 兼容代理(给 OpenClaw 用) ==========
@router.post("/v1/chat/completions", response_model=dict)
async def openai_proxy(request: dict):
"""
OpenAI 兼容端点 —— 代理到本地 Ollama
OpenClaw 可以配置 baseUrl 指向这里
"""
import httpx
from services.ai_command import OLLAMA_URL, OLLAMA_MODEL
model = request.get("model", OLLAMA_MODEL)
messages = request.get("messages", [])
max_tokens = request.get("max_tokens", 200)
try:
async with httpx.AsyncClient(timeout=60.0) as c:
r = await c.post(f"{OLLAMA_URL}/v1/chat/completions", json={
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"stream": False,
})
return r.json()
except Exception as e:
return {
"error": {"message": str(e), "type": "proxy_error"},
"choices": [],
}