131 lines
4.1 KiB
Python
131 lines
4.1 KiB
Python
"""
|
||
服务端编排:卡若 AI 网关决策 → WebSocket 下发到手机 Agent 执行。
|
||
手机无需本地 ai_api_key,统一走平常使用的 /api/gateway/chat。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
import os
|
||
from pathlib import Path
|
||
from typing import Any, Dict, Optional
|
||
import importlib.util
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_brain: Optional[Any] = None
|
||
|
||
|
||
def _load_ai_brain_class():
|
||
path = Path(__file__).resolve().parent.parent / "agent" / "ai_brain.py"
|
||
spec = importlib.util.spec_from_file_location("workphone_device_ai_brain", path)
|
||
if spec is None or spec.loader is None:
|
||
raise ImportError(f"无法加载 AIBrain: {path}")
|
||
mod = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(mod)
|
||
return mod.AIBrain
|
||
|
||
|
||
from services.ws_hub import ws_hub
|
||
|
||
_AGENT_CFG_CANDIDATES = [
|
||
Path(__file__).resolve().parent.parent / "agent" / "config.json",
|
||
Path(__file__).resolve().parents[2] / "agent" / "config.json",
|
||
]
|
||
|
||
|
||
def _load_ai_cfg() -> Dict[str, Any]:
|
||
for path in _AGENT_CFG_CANDIDATES:
|
||
try:
|
||
if path.is_file():
|
||
with open(path, encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
return data.get("ai_brain") or {}
|
||
except Exception as exc:
|
||
logger.warning("读取 %s 失败: %s", path, exc)
|
||
return {}
|
||
|
||
|
||
def get_karuo_brain(force_reload: bool = False) -> Optional[Any]:
|
||
global _brain
|
||
if _brain is not None and not force_reload:
|
||
return _brain
|
||
_brain = None
|
||
cfg = _load_ai_cfg()
|
||
api_key = (
|
||
os.environ.get("KARUO_API_KEY")
|
||
or os.environ.get("WP_AI_API_KEY")
|
||
or os.environ.get("AI_BRAIN_API_KEY")
|
||
or cfg.get("api_key")
|
||
or ""
|
||
).strip()
|
||
api_url = (
|
||
os.environ.get("KARUO_API_URL")
|
||
or os.environ.get("WP_AI_API_URL")
|
||
or os.environ.get("AI_BRAIN_API_URL")
|
||
or cfg.get("api_url")
|
||
or "http://127.0.0.1:3102"
|
||
).strip()
|
||
# 本机直跑 SDK(非 Docker 容器内)时 host.docker.internal 不可达
|
||
if "host.docker.internal" in api_url and not os.path.exists("/.dockerenv"):
|
||
api_url = api_url.replace("host.docker.internal", "127.0.0.1")
|
||
if not api_key:
|
||
logger.warning("卡若 AI Brain 未配置 api_key(sdk/agent/config.json)")
|
||
return None
|
||
_brain = _load_ai_brain_class()(
|
||
ai_api_url=api_url,
|
||
ai_api_key=api_key,
|
||
ai_model=cfg.get("model") or "auto",
|
||
brain_interval=int(cfg.get("brain_interval") or 60),
|
||
standing_orders=cfg.get("standing_orders") or [],
|
||
enabled=True,
|
||
)
|
||
return _brain
|
||
|
||
|
||
def device_status(device_id: str) -> Dict[str, Any]:
|
||
info = ws_hub.get_device_info(device_id) or {}
|
||
qs = info.get("quick_status") or info.get("last_status") or {}
|
||
return {
|
||
"device_id": device_id,
|
||
"online": ws_hub.is_online(device_id),
|
||
"model": info.get("model"),
|
||
"capabilities": info.get("capabilities") or [],
|
||
"u2": qs.get("u2", False),
|
||
"frida": info.get("frida_available", False),
|
||
"agent_version": info.get("agent_version"),
|
||
}
|
||
|
||
|
||
async def chat_and_execute_on_device(
|
||
device_id: str,
|
||
instruction: str,
|
||
timeout: int = 120,
|
||
) -> Dict[str, Any]:
|
||
brain = get_karuo_brain()
|
||
if not brain:
|
||
return {"code": 503, "message": "卡若 AI 未配置(agent/config.json ai_brain.api_key)"}
|
||
if not ws_hub.is_online(device_id):
|
||
return {"code": 503, "message": "设备不在线"}
|
||
|
||
async def execute_fn(script: str, action: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
return await ws_hub.send_command(
|
||
device_id,
|
||
{
|
||
"type": "execute",
|
||
"data": {
|
||
"script": script,
|
||
"action": action,
|
||
"params": params or {},
|
||
"channel": "auto",
|
||
},
|
||
},
|
||
timeout=timeout,
|
||
)
|
||
|
||
result = await brain.chat_and_execute(instruction.strip(), device_status(device_id), execute_fn)
|
||
result["device_id"] = device_id
|
||
result["orchestrator"] = "server_karuo_gateway"
|
||
return result
|