458 lines
18 KiB
Python
458 lines
18 KiB
Python
"""
|
||
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 已关闭")
|