Files
workphone-sdk/sdk/agent/ai_brain.py

458 lines
18 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.

"""
AI Brain — 设备端 AI 大脑(自主决策引擎)
架构定位:
┌──────────────────────────────────────────────────────┐
│ AI Brain (本模块) │
│ ┌────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 卡若AI API │ │ 任务队列 │ │ 离线缓冲 │ │
│ │ LLM 决策 │ │ 心跳驱动 │ │ 断连续航 │ │
│ └─────┬──────┘ └──────┬───────┘ └──────┬───────┘ │
│ └────────────────┼──────────────────┘ │
│ ↓ 决策结果: skill + action + params │
│ ┌────────────────┴──────────────────┐ │
│ │ Skill执行器(Frida优先/u2兜底) │ │
│ └───────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
核心能力:
1. 通过卡若AI API 获取 LLM 智能决策
2. 心跳驱动的自主运行循环(每 N 秒检查一次)
3. 离线模式:服务器断连时 AI 独立运行,结果缓冲待上传
4. Frida 优先通道:有 Frida 时优先用 Hook 操作,无则降级 u2
5. 任务队列:服务器推送 + AI 自主生成
卡若AI API:
POST {ai_api_url}/api/gateway/chat
Authorization: Bearer {ai_api_key}
Body: {"messages": [...], "max_tokens": 1024}
@author 卡若
@version 1.0.0
"""
import asyncio
import json
import logging
import time
import os
from typing import Optional, Dict, Any, List
from datetime import datetime
from collections import deque
logger = logging.getLogger(__name__)
try:
import aiohttp
except ImportError:
aiohttp = None
logger.warning("aiohttp 未安装AI Brain 网络请求将使用 urllib (pip install aiohttp)")
class TaskItem:
"""任务项"""
__slots__ = ("id", "source", "instruction", "skill_chain", "priority", "created_at", "status", "result")
def __init__(self, instruction: str, source: str = "ai", priority: int = 5):
self.id = f"task_{int(time.time() * 1000)}"
self.source = source # "server" | "ai" | "standing_order"
self.instruction = instruction
self.skill_chain: List[Dict[str, Any]] = [] # AI 解析后的执行链
self.priority = priority # 1-10, 10=最高
self.created_at = time.time()
self.status = "pending" # pending → running → done / failed
self.result: Optional[Dict] = None
class AIBrain:
"""
设备端 AI 大脑 — 心跳驱动的自主决策引擎
工作模式:
- online: 服务器在线,接收推送任务 + AI 辅助决策
- offline: 服务器断连AI 独立运行,执行 standing_orders + 自主决策
"""
VERSION = "1.0.0"
SYSTEM_PROMPT = """你是一个运行在 Android 手机上的 AI 助手。你的任务是根据当前手机状态和待办事项,决定下一步要执行的操作。
你可以控制以下 APP微信(wechat)、抖音(douyin)、小红书(xhs)、闲鱼(xianyu)、Soul(soul)。
每个 APP 支持的操作(action)
- wechat: send_message, get_messages, get_contacts, add_friend, accept_friend, post_moment, like_moment, get_groups,
check_login_state, ensure_logged_in, login_by_password, unblock_via_customer_service
- douyin: send_message, get_messages, get_fans, reply_comment
- xhs: send_message, get_messages, like_note
- xianyu: send_message, get_messages
- soul: send_message, get_messages
通用操作screenshot, click, input, swipe, app_start, app_stop, device_info
你必须以 JSON 格式回复,结构如下:
{
"should_act": true/false,
"reason": "决策原因",
"actions": [
{"script": "wechat", "action": "send_message", "params": {"to": "xxx", "content": "xxx"}},
...
]
}
如果当前没有需要执行的任务,返回 {"should_act": false, "reason": "无待处理任务"}。
如果设备状态异常(低电量、无网络),优先处理设备问题。"""
def __init__(
self,
ai_api_url: str = "http://localhost:3102",
ai_api_key: str = "",
ai_model: str = "auto",
brain_interval: int = 60,
max_offline_buffer: int = 500,
standing_orders: Optional[List[str]] = None,
enabled: bool = True,
):
self.ai_api_url = ai_api_url.rstrip("/")
self.ai_api_key = ai_api_key
self.ai_model = ai_model
self.brain_interval = brain_interval # AI 思考间隔(秒)
self.enabled = enabled
self.task_queue: deque = deque(maxlen=200)
self.offline_buffer: deque = deque(maxlen=max_offline_buffer)
self.standing_orders = standing_orders or []
self._online = True
self._running = False
self._last_think_time = 0
self._think_count = 0
self._execute_count = 0
self._session: Optional[Any] = None # aiohttp.ClientSession
logger.info(f"🧠 AI Brain v{self.VERSION} 初始化")
logger.info(f" 卡若网关: {self.ai_api_url}/api/gateway/chat")
logger.info(f" 间隔: {self.brain_interval}s")
logger.info(f" 常驻指令: {len(self.standing_orders)}")
logger.info(f" 启用: {self.enabled}")
@property
def online(self) -> bool:
return self._online
@online.setter
def online(self, value: bool):
if self._online != value:
self._online = value
mode = "在线" if value else "离线(自主运行)"
logger.info(f"🧠 AI Brain 模式切换: {mode}")
def add_task(self, instruction: str, source: str = "server", priority: int = 5):
"""添加任务到队列"""
task = TaskItem(instruction=instruction, source=source, priority=priority)
self.task_queue.append(task)
logger.info(f"📋 新任务入队: [{source}] {instruction[:50]}...")
return task.id
def add_standing_order(self, order: str):
"""添加常驻指令(离线时自动执行)"""
if order not in self.standing_orders:
self.standing_orders.append(order)
logger.info(f"📌 新增常驻指令: {order[:50]}...")
def buffer_offline_result(self, result: Dict):
"""缓冲离线执行结果(待服务器重连后上传)"""
result["buffered_at"] = time.time()
self.offline_buffer.append(result)
def flush_offline_buffer(self) -> List[Dict]:
"""刷出离线缓冲(重连后调用)"""
results = list(self.offline_buffer)
self.offline_buffer.clear()
return results
async def _get_session(self):
"""获取/创建 HTTP session"""
if aiohttp and (self._session is None or self._session.closed):
timeout = aiohttp.ClientTimeout(total=30)
self._session = aiohttp.ClientSession(timeout=timeout)
return self._session
async def call_ai(self, messages: List[Dict[str, str]], max_tokens: int = 1024) -> Optional[str]:
"""调用卡若AI 统一网关 POST /api/gateway/chat"""
import time as _t
# 429 限流冷却:命中后进入冷却窗,期间跳过规划调用,避免突发重试刷屏与配额浪费
_cd = getattr(self, "_rate_limited_until", 0.0)
if _t.time() < _cd:
return None
url = f"{self.ai_api_url}/api/gateway/chat"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.ai_api_key}",
}
payload = {
"messages": messages,
"max_tokens": max_tokens,
}
if self.ai_model and self.ai_model != "auto":
payload["model"] = self.ai_model
try:
if aiohttp:
session = await self._get_session()
async with session.post(url, json=payload, headers=headers) as resp:
if resp.status != 200:
body = await resp.text()
# 任何非 200 都进入冷却,避免在额度不足/网关故障时每数秒狂刷,
# 把 asyncio 事件循环饿死、拖垮 hook/连接守护。
if resp.status == 429:
cd = 120
elif resp.status in (401, 402, 403) or "额度" in body or "insufficient" in body.lower():
cd = 600
else:
cd = 120
self._rate_limited_until = _t.time() + cd
logger.warning(f"AI API {resp.status} → 进入 {cd}s 冷却(不影响 hook/连接守护): {body[:160]}")
return None
data = await resp.json()
else:
import urllib.request
req = urllib.request.Request(
url,
data=json.dumps(payload).encode(),
headers=headers,
method="POST",
)
loop = asyncio.get_event_loop()
with await loop.run_in_executor(None, urllib.request.urlopen, req) as resp:
data = json.loads(resp.read().decode())
content = (
data.get("choices", [{}])[0]
.get("message", {})
.get("content", "")
)
return content
except Exception as e:
# 网络异常同样退避,避免异常风暴拖垮事件循环
self._rate_limited_until = _t.time() + 60
logger.error(f"AI API 调用失败 → 60s 冷却: {e}")
return None
async def think(self, device_status: Dict, pending_instructions: Optional[List[str]] = None) -> Optional[Dict]:
"""
AI 思考:基于当前设备状态 + 待办任务,决定下一步操作
返回: {"should_act": bool, "reason": str, "actions": [...]} 或 None
"""
if not self.enabled:
return None
self._last_think_time = time.time()
self._think_count += 1
context_parts = [f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"]
context_parts.append(f"设备状态: {json.dumps(device_status, ensure_ascii=False, default=str)}")
if not self._online:
context_parts.append("⚠️ 当前处于离线模式(服务器未连接),需要自主决策")
if pending_instructions:
context_parts.append(f"待处理任务: {json.dumps(pending_instructions, ensure_ascii=False)}")
if self.standing_orders and not self._online:
context_parts.append(f"常驻指令(离线时执行): {json.dumps(self.standing_orders, ensure_ascii=False)}")
user_message = "\n".join(context_parts)
messages = [
{"role": "system", "content": self.SYSTEM_PROMPT},
{"role": "user", "content": user_message},
]
response = await self.call_ai(messages, max_tokens=1024)
if not response:
return None
try:
start = response.find("{")
end = response.rfind("}") + 1
if start >= 0 and end > start:
decision = json.loads(response[start:end])
logger.info(f"🧠 AI 决策: should_act={decision.get('should_act')}, reason={decision.get('reason', '')[:60]}")
return decision
except json.JSONDecodeError as e:
logger.warning(f"AI 响应解析失败: {e}, raw={response[:200]}")
return None
async def heartbeat_cycle(self, device_status: Dict, execute_fn) -> Dict:
"""
心跳驱动的 AI 循环(每次心跳调用一次)
Args:
device_status: 当前设备状态
execute_fn: 执行函数 async (script, action, params) -> result
Returns:
{"thought": bool, "acted": bool, "results": [...]}
"""
if not self.enabled:
return {"thought": False, "acted": False, "results": []}
now = time.time()
if now - self._last_think_time < self.brain_interval:
return {"thought": False, "acted": False, "results": [], "skip": "间隔未到"}
pending = [t.instruction for t in self.task_queue if t.status == "pending"]
decision = await self.think(device_status, pending if pending else None)
if not decision or not decision.get("should_act"):
return {"thought": True, "acted": False, "results": [], "reason": decision.get("reason") if decision else "AI无响应"}
results = []
actions = decision.get("actions", [])
for action_spec in actions:
script = action_spec.get("script", "")
action = action_spec.get("action", "")
params = action_spec.get("params", {})
if not script or not action:
continue
try:
result = await execute_fn(script, action, params)
self._execute_count += 1
entry = {
"script": script,
"action": action,
"params": params,
"result": result,
"timestamp": time.time(),
}
results.append(entry)
if not self._online:
self.buffer_offline_result(entry)
except Exception as e:
logger.error(f"AI Brain 执行失败 [{script}.{action}]: {e}")
results.append({
"script": script, "action": action,
"error": str(e), "timestamp": time.time(),
})
for task in list(self.task_queue):
if task.status == "pending":
task.status = "done"
task.result = {"actions": len(actions), "results_count": len(results)}
return {"thought": True, "acted": True, "results": results, "reason": decision.get("reason", "")}
async def autonomous_loop(self, device_status_fn, execute_fn, stop_event: asyncio.Event):
"""
离线自主运行循环 — 服务器断连时启动
持续运行直到 stop_event 被设置(通常是服务器重连时)
Args:
device_status_fn: 获取设备状态的函数 () -> dict
execute_fn: 执行函数 async (script, action, params) -> result
stop_event: 停止信号
"""
logger.info("🧠 AI Brain 进入自主运行模式")
self.online = False
cycle = 0
while not stop_event.is_set():
cycle += 1
try:
status = device_status_fn()
battery = status.get("battery_level", 100)
if battery < 10:
logger.warning(f"⚡ 电量过低 ({battery}%),暂停自主操作")
await asyncio.sleep(self.brain_interval * 2)
continue
result = await self.heartbeat_cycle(status, execute_fn)
if result.get("acted"):
logger.info(f"🧠 自主执行第{cycle}轮: {len(result.get('results', []))}个操作")
except Exception as e:
logger.error(f"自主运行循环异常: {e}")
try:
await asyncio.wait_for(stop_event.wait(), timeout=self.brain_interval)
break
except asyncio.TimeoutError:
pass
logger.info("🧠 AI Brain 退出自主运行模式")
self.online = True
async def chat_and_execute(
self,
user_instruction: str,
device_status: Dict,
execute_fn,
) -> Dict:
"""用户对话:卡若网关立即决策并执行"""
self.add_task(user_instruction, source="user", priority=10)
decision = await self.think(device_status, [user_instruction])
if not decision:
return {"code": 503, "message": "卡若AI 无响应", "instruction": user_instruction}
if not decision.get("should_act"):
return {
"code": 200,
"acted": False,
"reason": decision.get("reason", ""),
"instruction": user_instruction,
}
results = []
for action_spec in decision.get("actions", []):
script = action_spec.get("script", "")
action = action_spec.get("action", "")
params = action_spec.get("params", {})
if not script or not action:
continue
try:
result = await execute_fn(script, action, params)
self._execute_count += 1
results.append({"script": script, "action": action, "result": result})
except Exception as exc:
results.append({"script": script, "action": action, "error": str(exc)})
return {
"code": 200,
"acted": True,
"results": results,
"reason": decision.get("reason", ""),
"instruction": user_instruction,
"gateway": f"{self.ai_api_url}/api/gateway/chat",
}
def get_status(self) -> Dict:
"""获取 AI Brain 状态"""
return {
"enabled": self.enabled,
"online": self._online,
"running": self._running,
"brain_interval": self.brain_interval,
"think_count": self._think_count,
"execute_count": self._execute_count,
"task_queue_size": len(self.task_queue),
"offline_buffer_size": len(self.offline_buffer),
"standing_orders": len(self.standing_orders),
"last_think_time": self._last_think_time,
"ai_api_url": self.ai_api_url,
"ai_model": self.ai_model,
"gateway": f"{self.ai_api_url}/api/gateway/chat",
"version": self.VERSION,
}
async def close(self):
"""关闭资源"""
if self._session and not self._session.closed:
await self._session.close()
self._running = False
logger.info("🧠 AI Brain 已关闭")