Some checks failed
SDK CI / python-compile (push) Has been cancelled
- 新增 sdk/app/agent(Hook/Skills/Anti-ban 等)及 NAS/ADB 脚本 - 更新 Android 端、unified、PHP SDK、Docker Compose - Soul 文档迁移至 Soul调研/;移除资料目录内 APK - .gitignore 排除 sdk/tmp、sdk/logs、sdk/tmp_rom Co-authored-by: Cursor <cursoragent@cursor.com>
1490 lines
61 KiB
Python
1490 lines
61 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
工作手机Agent v3.1 - AI数字员工(Frida + AI Brain 增强版)
|
||
运行在Android手机上,主动连接SDK服务器,保持实时通信
|
||
内置 AI 大脑,支持离线自主运行
|
||
|
||
核心架构:
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ 手机端 Agent (Python/Termux) v3.1 │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │
|
||
│ │ WebSocket │ │ AI Brain │ │ Frida Hook │ │
|
||
│ │ 服务器连接 │ │ 自主决策 │ │ APP控制 │ │
|
||
│ └──────┬──────┘ └──────┬──────┘ └──────┬───────┘ │
|
||
│ │ │ │ │
|
||
│ └────────────────┼────────────────┘ │
|
||
│ ↕ Frida RPC (优先) / u2 (兜底) │
|
||
│ ┌────────────────────────────────────┐ │
|
||
│ │ Android系统 + 微信/抖音等APP │ │
|
||
│ └────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────┘
|
||
↕ WebSocket (wss://)
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ SDK服务器 (FastAPI) │
|
||
│ 存客宝/触客宝通过API调用 → 服务器下发命令到手机 │
|
||
└──────────────────────────────────────────────────────┘
|
||
|
||
运行模式:
|
||
- 在线模式: 服务器连接 → 接收指令 + AI 辅助 + Frida 执行
|
||
- 离线模式: 服务器断连 → AI Brain 自主决策 → Frida/u2 执行 → 结果缓冲
|
||
|
||
连接机制:
|
||
- 手机主动发起WebSocket连接到服务器
|
||
- 心跳保活: 每N秒发送心跳包,服务器响应超时则重连
|
||
- 指数退避重连: 2s → 4s → 8s → 16s → 30s(最大)
|
||
- 断线后 AI Brain 接管,自主运行直到重连
|
||
- Frida 优先通道: Hook > u2 > 降级
|
||
|
||
配置优先级: 环境变量 > 命令行参数 > config.json > 默认值
|
||
WP_DEVICE_ID 设备ID
|
||
WP_SERVER_URL WebSocket基础地址(如 ws://192.168.1.100:8899/ws/device)
|
||
WP_PROJECT_ID 项目ID
|
||
WP_HEARTBEAT 心跳间隔(秒)
|
||
WP_AI_API_URL 卡若AI API 地址
|
||
WP_AI_API_KEY 卡若AI API 密钥
|
||
|
||
@author 卡若
|
||
@version 3.1.0
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import argparse
|
||
import os
|
||
import sys
|
||
import signal
|
||
import time
|
||
import base64
|
||
import random
|
||
from datetime import datetime
|
||
from typing import Optional, Dict, Any
|
||
|
||
# 确保 agent/ 目录在 sys.path 中,方便 Termux 等环境直接运行
|
||
_AGENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
if _AGENT_DIR not in sys.path:
|
||
sys.path.insert(0, _AGENT_DIR)
|
||
|
||
try:
|
||
import websockets
|
||
except ImportError:
|
||
print("❌ 请安装: pip install websockets>=12.0")
|
||
sys.exit(1)
|
||
|
||
try:
|
||
import uiautomator2 as u2
|
||
except ImportError:
|
||
u2 = None
|
||
print("⚠️ uiautomator2未安装,部分功能不可用(pip install uiautomator2>=3.0.0)")
|
||
|
||
# 配置日志
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s [%(levelname)s] %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class WorkPhoneAgent:
|
||
"""
|
||
AI数字员工 - 手机端Agent(Frida + AI Brain 增强版)
|
||
|
||
核心职责:
|
||
1. 主动连接SDK服务器并保持实时通信
|
||
2. 接收服务器命令并在手机上执行(Frida优先 → u2兜底)
|
||
3. 实时上报设备状态(电量/网络/APP状态)
|
||
4. 支持微信/抖音/小红书等多APP控制
|
||
5. 内置AI大脑,心跳驱动自主决策
|
||
6. 离线模式:服务器断连时AI独立运行
|
||
"""
|
||
|
||
VERSION = "3.1.0"
|
||
|
||
MIN_RECONNECT_DELAY = 2
|
||
MAX_RECONNECT_DELAY = 30
|
||
|
||
def __init__(
|
||
self,
|
||
device_id: str,
|
||
server_url: str,
|
||
heartbeat_interval: int = 30,
|
||
project_id: str = "default",
|
||
ai_config: Optional[Dict[str, Any]] = None,
|
||
):
|
||
self.device_id = device_id
|
||
self.server_url = server_url
|
||
self.heartbeat_interval = heartbeat_interval
|
||
self.project_id = project_id
|
||
|
||
self.ws: Optional[websockets.WebSocketClientProtocol] = None
|
||
self.running = False
|
||
self.connected = False
|
||
self.reconnect_attempts = 0
|
||
self.last_heartbeat_ack = time.time()
|
||
self.commands_executed = 0
|
||
self.start_time = None
|
||
|
||
# 设备控制通道(u2 优先,降级 LocalDevice)
|
||
self.d = None
|
||
self._init_u2()
|
||
|
||
# Frida Manager(优先通道)
|
||
self.frida_mgr = None
|
||
self._init_frida()
|
||
|
||
# ── 设备端深层防护模块 ──
|
||
self.device_guard = None
|
||
self.risk_sentinel = None
|
||
self.sensor_sim = None
|
||
self.touch_hardener = None
|
||
self.nurture_scheduler = None
|
||
self._init_anti_ban()
|
||
|
||
# AI Brain(自主决策引擎)
|
||
self.ai_brain = None
|
||
self._autonomous_stop = asyncio.Event()
|
||
self._autonomous_task: Optional[asyncio.Task] = None
|
||
ai_cfg = ai_config or {}
|
||
if ai_cfg.get("enabled", False):
|
||
self._init_ai_brain(ai_cfg)
|
||
|
||
logger.info(f"🤖 AI数字员工 v{self.VERSION} 初始化完成")
|
||
logger.info(f" 设备ID: {device_id}")
|
||
logger.info(f" 服务器: {server_url}")
|
||
logger.info(f" 心跳间隔: {heartbeat_interval}秒")
|
||
logger.info(f" Frida: {'✅ 已加载' if self.frida_mgr else '❌ 未加载'}")
|
||
logger.info(f" 防护: {'✅' if self.device_guard else '❌'} Guard | "
|
||
f"{'✅' if self.risk_sentinel else '❌'} Sentinel | "
|
||
f"{'✅' if self.sensor_sim else '❌'} Sensor | "
|
||
f"{'✅' if self.touch_hardener else '❌'} Touch")
|
||
logger.info(f" AI Brain: {'✅ 已启用' if self.ai_brain else '⬜ 未启用'}")
|
||
|
||
def _init_u2(self):
|
||
"""
|
||
初始化设备控制(优先 u2,降级到 LocalDevice)
|
||
|
||
连接策略:
|
||
1. 有 ADB 环境 → u2.connect(serial)
|
||
2. ADB 不可用 → LocalDevice(ATX HTTP 直连)
|
||
3. ATX 也不可用 → self.d 保持 None,运行时重试
|
||
"""
|
||
import shutil
|
||
has_adb = shutil.which("adb") is not None
|
||
|
||
if has_adb and u2:
|
||
try:
|
||
self.d = u2.connect(self.device_id)
|
||
self.d.implicitly_wait(10.0)
|
||
self.d.settings['operation_delay'] = (0, 0)
|
||
self.d.settings['operation_delay_methods'] = []
|
||
logger.info(f"u2 通过 ADB 连接: {self.d.info.get('productName', 'Unknown')}")
|
||
return
|
||
except Exception as e:
|
||
logger.info(f"u2 ADB 连接失败: {e},尝试 LocalDevice")
|
||
|
||
try:
|
||
from local_device import LocalDevice
|
||
self.d = LocalDevice()
|
||
logger.info("✅ LocalDevice 已启动(ATX HTTP 直连)")
|
||
except Exception as e:
|
||
logger.warning(f"LocalDevice 初始化失败: {e}(将在运行时重试)")
|
||
|
||
def _init_frida(self):
|
||
"""初始化 Frida Manager(优先控制通道)"""
|
||
try:
|
||
from hook.frida_manager import FridaManager
|
||
import os as _os
|
||
_mode = (_os.environ.get("WP_FRIDA_MODE") or "gadget").strip().lower()
|
||
if _mode not in ("usb", "gadget", "remote"):
|
||
_mode = "gadget"
|
||
_serial = _os.environ.get("WP_DEVICE_SERIAL") or (
|
||
self.device_id if _mode == "usb" else None
|
||
)
|
||
self.frida_mgr = FridaManager(
|
||
device_serial=_serial,
|
||
mode=_mode,
|
||
on_event=self._on_frida_event,
|
||
auto_reconnect=True,
|
||
)
|
||
started = self.frida_mgr.start()
|
||
if started:
|
||
logger.info("🔗 Frida 控制通道已就绪")
|
||
else:
|
||
logger.warning("⚠️ Frida 启动失败,将使用 u2 通道")
|
||
self.frida_mgr = None
|
||
except ImportError:
|
||
logger.info("Frida 模块未安装,跳过 Hook 通道")
|
||
except Exception as e:
|
||
logger.warning(f"Frida 初始化异常: {e}")
|
||
self.frida_mgr = None
|
||
|
||
def _on_frida_event(self, payload: dict):
|
||
"""Frida 事件回调(Hook 事件上报)"""
|
||
event_type = payload.get("event", "unknown")
|
||
logger.debug(f"[Frida事件] {event_type}: {str(payload)[:100]}")
|
||
if self.connected and self.ws:
|
||
asyncio.ensure_future(self._send_event(f"hook_{event_type}", payload))
|
||
|
||
def _init_anti_ban(self):
|
||
"""初始化设备端深层防护模块"""
|
||
if not self.d:
|
||
logger.warning("⚠️ u2 未连接,防护模块跳过初始化")
|
||
return
|
||
|
||
try:
|
||
from anti_ban.device_guard import DeviceGuard
|
||
self.device_guard = DeviceGuard(self.d)
|
||
guard_report = self.device_guard.run_full_check()
|
||
if guard_report.get("warnings"):
|
||
logger.warning(f"🛡️ 设备自检发现 {guard_report['warning_count']} 项警告")
|
||
except Exception as e:
|
||
logger.error(f"DeviceGuard 初始化失败: {e}")
|
||
|
||
try:
|
||
from anti_ban.risk_sentinel import RiskSentinel
|
||
self.risk_sentinel = RiskSentinel()
|
||
except Exception as e:
|
||
logger.error(f"RiskSentinel 初始化失败: {e}")
|
||
|
||
try:
|
||
from anti_ban.sensor_simulator import SensorSimulator
|
||
self.sensor_sim = SensorSimulator(device=self.d)
|
||
except Exception as e:
|
||
logger.error(f"SensorSimulator 初始化失败: {e}")
|
||
|
||
try:
|
||
from anti_ban.touch_hardener import TouchHardener
|
||
self.touch_hardener = TouchHardener(self.d)
|
||
except Exception as e:
|
||
logger.error(f"TouchHardener 初始化失败: {e}")
|
||
|
||
try:
|
||
from anti_ban.nurture_scheduler import NurtureScheduler
|
||
self.nurture_scheduler = NurtureScheduler()
|
||
except Exception as e:
|
||
logger.error(f"NurtureScheduler 初始化失败: {e}")
|
||
|
||
def _on_risk_alert(self, level: int, message: str, details: dict):
|
||
"""风控告警回调 — 上报服务器"""
|
||
logger.warning(f"🚨 风控告警 Lv{level}: {message}")
|
||
if self.connected and self.ws:
|
||
asyncio.ensure_future(self._send_event("risk_alert", {
|
||
"level": level,
|
||
"message": message,
|
||
"details": details,
|
||
}))
|
||
|
||
def _init_ai_brain(self, ai_cfg: dict):
|
||
"""初始化 AI Brain"""
|
||
try:
|
||
from ai_brain import AIBrain
|
||
self.ai_brain = AIBrain(
|
||
ai_api_url=ai_cfg.get("api_url", "http://localhost:3102"),
|
||
ai_api_key=ai_cfg.get("api_key", ""),
|
||
ai_model=ai_cfg.get("model", "auto"),
|
||
brain_interval=ai_cfg.get("brain_interval", 60),
|
||
standing_orders=ai_cfg.get("standing_orders", []),
|
||
enabled=True,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"AI Brain 初始化失败: {e}")
|
||
self.ai_brain = None
|
||
|
||
# ====================================================================
|
||
# 一、连接管理(核心:主动连接 + 心跳保活 + 指数退避重连)
|
||
# ====================================================================
|
||
|
||
async def connect(self):
|
||
"""主连接循环 - 永不停止,断连时切换离线AI自主模式"""
|
||
while self.running:
|
||
try:
|
||
logger.info(f"📡 正在连接服务器: {self.server_url}")
|
||
|
||
# 重连成功 → 停止离线自主模式
|
||
self._stop_autonomous_mode()
|
||
|
||
async with websockets.connect(
|
||
self.server_url,
|
||
ping_interval=20,
|
||
ping_timeout=10,
|
||
close_timeout=5,
|
||
max_size=10 * 1024 * 1024,
|
||
) as ws:
|
||
self.ws = ws
|
||
self.connected = True
|
||
self.reconnect_attempts = 0
|
||
self.last_heartbeat_ack = time.time()
|
||
|
||
logger.info("✅ 服务器连接成功!")
|
||
|
||
if self.ai_brain:
|
||
self.ai_brain.online = True
|
||
|
||
# 1. 注册 + 上报 + 拉配置
|
||
await self._register()
|
||
await self._send_event("agent_started", {
|
||
"device_id": self.device_id,
|
||
"project_id": self.project_id,
|
||
"frida_available": self.frida_mgr is not None and self.frida_mgr.connected,
|
||
"ai_brain_enabled": self.ai_brain is not None,
|
||
"anti_ban": {
|
||
"device_guard": self.device_guard.report if self.device_guard else None,
|
||
"risk_sentinel": self.risk_sentinel is not None,
|
||
"sensor_sim": self.sensor_sim is not None,
|
||
"touch_hardener": self.touch_hardener is not None,
|
||
"nurture_scheduler": self.nurture_scheduler is not None,
|
||
},
|
||
})
|
||
await self._send_device_request("get_config", {})
|
||
|
||
# 2. 上传离线缓冲结果
|
||
await self._flush_offline_buffer()
|
||
|
||
# 3. 启动并发任务(含连接守护)
|
||
tasks = [
|
||
asyncio.create_task(self._heartbeat_loop()),
|
||
asyncio.create_task(self._status_report_loop()),
|
||
asyncio.create_task(self._message_loop()),
|
||
asyncio.create_task(self._connection_guard_loop()),
|
||
]
|
||
if self.ai_brain:
|
||
tasks.append(asyncio.create_task(self._ai_brain_loop()))
|
||
if self.nurture_scheduler:
|
||
tasks.append(asyncio.create_task(self._nurture_loop()))
|
||
|
||
done, pending = await asyncio.wait(
|
||
tasks, return_when=asyncio.FIRST_COMPLETED
|
||
)
|
||
|
||
for task in pending:
|
||
task.cancel()
|
||
try:
|
||
await task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
|
||
except websockets.ConnectionClosed as e:
|
||
logger.warning(f"🔌 连接断开: code={e.code}, reason={e.reason}")
|
||
except ConnectionRefusedError:
|
||
logger.warning("🚫 服务器拒绝连接")
|
||
except OSError as e:
|
||
logger.warning(f"🌐 网络错误: {e}")
|
||
except Exception as e:
|
||
logger.error(f"❌ 连接异常: {e}")
|
||
finally:
|
||
self.connected = False
|
||
self.ws = None
|
||
|
||
if self.running:
|
||
self.reconnect_attempts += 1
|
||
|
||
# 断连后启动离线自主模式(AI Brain 接管)
|
||
self._start_autonomous_mode()
|
||
|
||
# 尝试恢复网络
|
||
if self.d:
|
||
try:
|
||
from hawk import try_reconnect_network
|
||
nr = try_reconnect_network(self.d)
|
||
if nr.get("success"):
|
||
logger.info(f"🌐 网络已恢复: {nr.get('message', '')}")
|
||
else:
|
||
logger.info(f"🌐 网络恢复: {nr.get('message', '')}")
|
||
except ImportError:
|
||
pass
|
||
except Exception as e:
|
||
logger.debug(f"网络恢复尝试异常: {e}")
|
||
|
||
delay = self._get_reconnect_delay()
|
||
logger.info(f"⏳ {delay:.1f}秒后第{self.reconnect_attempts}次重连...")
|
||
await asyncio.sleep(delay)
|
||
|
||
def _start_autonomous_mode(self):
|
||
"""启动离线自主运行模式"""
|
||
if not self.ai_brain or self._autonomous_task:
|
||
return
|
||
self._autonomous_stop.clear()
|
||
self._autonomous_task = asyncio.ensure_future(
|
||
self.ai_brain.autonomous_loop(
|
||
device_status_fn=self._get_quick_status,
|
||
execute_fn=self._execute_with_frida_priority,
|
||
stop_event=self._autonomous_stop,
|
||
)
|
||
)
|
||
logger.info("🧠 已启动离线自主运行模式")
|
||
|
||
def _stop_autonomous_mode(self):
|
||
"""停止离线自主运行模式"""
|
||
if self._autonomous_task:
|
||
self._autonomous_stop.set()
|
||
self._autonomous_task = None
|
||
logger.info("🧠 已停止离线自主运行模式")
|
||
|
||
async def _flush_offline_buffer(self):
|
||
"""上传离线期间缓冲的执行结果"""
|
||
if not self.ai_brain:
|
||
return
|
||
buffered = self.ai_brain.flush_offline_buffer()
|
||
if not buffered:
|
||
return
|
||
logger.info(f"📤 上传 {len(buffered)} 条离线缓冲结果")
|
||
try:
|
||
await self._send_event("offline_buffer_upload", {
|
||
"count": len(buffered),
|
||
"results": buffered[-50:], # 最多上传最近50条
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"离线缓冲上传失败: {e}")
|
||
|
||
def _get_reconnect_delay(self) -> float:
|
||
"""计算重连延迟(指数退避 + 随机抖动)"""
|
||
delay = min(
|
||
self.MIN_RECONNECT_DELAY * (2 ** self.reconnect_attempts),
|
||
self.MAX_RECONNECT_DELAY
|
||
)
|
||
# 添加随机抖动避免雪崩
|
||
jitter = random.uniform(0, delay * 0.2)
|
||
return delay + jitter
|
||
|
||
async def _register(self):
|
||
"""发送设备注册信息"""
|
||
device_info = self._get_device_info()
|
||
|
||
await self.ws.send(json.dumps({
|
||
"type": "register",
|
||
"data": {
|
||
**device_info,
|
||
"project_id": self.project_id,
|
||
"agent_version": self.VERSION,
|
||
"registered_at": datetime.now().isoformat(),
|
||
}
|
||
}))
|
||
|
||
logger.info(f"📋 设备已注册: {device_info.get('model', 'Unknown')}")
|
||
|
||
# ====================================================================
|
||
# 一B、AI Brain 循环 + Frida 优先执行
|
||
# ====================================================================
|
||
|
||
async def _ai_brain_loop(self):
|
||
"""AI Brain 在线模式循环 — 随心跳周期运行"""
|
||
if not self.ai_brain:
|
||
return
|
||
|
||
while self.connected:
|
||
try:
|
||
await asyncio.sleep(self.ai_brain.brain_interval)
|
||
if not self.connected:
|
||
break
|
||
|
||
status = self._get_quick_status()
|
||
result = await self.ai_brain.heartbeat_cycle(
|
||
device_status=status,
|
||
execute_fn=self._execute_with_frida_priority,
|
||
)
|
||
if result.get("acted"):
|
||
logger.info(f"🧠 AI Brain 在线执行: {len(result.get('results', []))}个操作")
|
||
await self._send_event("ai_brain_acted", {
|
||
"results_count": len(result.get("results", [])),
|
||
"reason": result.get("reason", ""),
|
||
})
|
||
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"AI Brain 循环异常: {e}")
|
||
await asyncio.sleep(10)
|
||
|
||
async def _nurture_loop(self):
|
||
"""养号调度循环 — 每 15 分钟检查一次"""
|
||
while self.connected:
|
||
try:
|
||
await asyncio.sleep(random.randint(800, 1000))
|
||
if not self.connected:
|
||
break
|
||
if self.risk_sentinel:
|
||
stats = self.risk_sentinel.get_stats()
|
||
if stats.get("_total_ops", 0) > 500:
|
||
continue
|
||
if self.nurture_scheduler:
|
||
plan = self.nurture_scheduler.get_nurture_plan(self.device_id)
|
||
if plan:
|
||
item = plan[0]
|
||
logger.info(f"🌱 养号计划: {item.get('action', '?')}")
|
||
await self._send_event("nurture_plan", {"plan": plan})
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"养号循环异常: {e}")
|
||
await asyncio.sleep(60)
|
||
|
||
async def _execute_with_frida_priority(
|
||
self, script: str, action: str, params: dict, hook_only: bool = False
|
||
) -> dict:
|
||
"""
|
||
Frida 优先执行通道:
|
||
1. 优先走 Frida RPC(Hook 级别,直接操作 APP 内部)
|
||
2. Frida 不可用时降级到 u2(UI自动化)
|
||
3. hook_only=True 时禁止降级(纯 Hawk Hook 联调)
|
||
"""
|
||
# 通道1: Frida Hook(仅微信脚本有 wechat_hook RPC 映射)
|
||
if self.frida_mgr and self.frida_mgr.connected and script == "wechat":
|
||
try:
|
||
from hook.hook_executor import HookExecutor
|
||
executor = HookExecutor(self.frida_mgr)
|
||
frida_result = executor.execute(action, params)
|
||
if frida_result and frida_result.get("success", False):
|
||
logger.debug(f"[Frida] {script}.{action} 执行成功")
|
||
return {"code": 200, "data": frida_result, "channel": "frida"}
|
||
logger.debug(f"[Frida] {script}.{action} 执行失败,降级 u2")
|
||
except ImportError:
|
||
pass
|
||
except Exception as e:
|
||
logger.warning(f"[Frida] {script}.{action} 异常: {e},降级 u2")
|
||
|
||
if hook_only:
|
||
return {
|
||
"code": 503,
|
||
"message": "Hook 不可用或 RPC 未成功(hook_only 禁止 u2/ADB 降级)",
|
||
"data": {
|
||
"script": script,
|
||
"action": action,
|
||
"frida_connected": bool(
|
||
self.frida_mgr and self.frida_mgr.connected
|
||
),
|
||
},
|
||
}
|
||
|
||
# 通道2: u2 Skill(兜底)
|
||
return await self._execute_skill(script, action, params)
|
||
|
||
# ====================================================================
|
||
# 二、心跳机制(参考抖音心跳: 应用层心跳 + 状态上报)
|
||
# ====================================================================
|
||
|
||
async def _heartbeat_loop(self):
|
||
"""
|
||
轻量心跳循环 — 不做任何 u2/ADB I/O,仅发送业务层心跳并检测 ACK。
|
||
u2 是非线程安全的,并发访问会导致 HTTP 请求挂起。
|
||
守护任务(弹窗/网络/状态)由 _connection_guard_loop 独立处理。
|
||
"""
|
||
missed_count = 0
|
||
|
||
while self.connected:
|
||
try:
|
||
await asyncio.sleep(self.heartbeat_interval)
|
||
|
||
if not self.connected or not self.ws:
|
||
break
|
||
|
||
heartbeat_data = {
|
||
"type": "heartbeat",
|
||
"timestamp": int(time.time()),
|
||
"device_id": self.device_id,
|
||
"uptime": int(time.time() - self.start_time) if self.start_time else 0,
|
||
"commands_executed": self.commands_executed,
|
||
"status": {"online": True, "u2": bool(self.d)},
|
||
}
|
||
|
||
await self.ws.send(json.dumps(heartbeat_data))
|
||
|
||
# 检查 ACK(宽容模式:5 分钟无 ACK 才断连,避免 u2 竞争导致误判)
|
||
time_since_ack = time.time() - self.last_heartbeat_ack
|
||
if time_since_ack > 300:
|
||
missed_count += 1
|
||
logger.warning(f"⚠️ 心跳无响应 ({missed_count}/3),上次ACK: {time_since_ack:.0f}秒前")
|
||
|
||
if missed_count >= 3:
|
||
logger.error("💔 连续3次心跳无响应,主动断开重连")
|
||
await self.ws.close()
|
||
break
|
||
else:
|
||
missed_count = 0
|
||
|
||
except websockets.ConnectionClosed:
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"心跳错误: {e}")
|
||
break
|
||
|
||
def _try_reconnect_u2(self):
|
||
"""尝试重新连接设备控制(u2 或 LocalDevice)"""
|
||
import shutil
|
||
has_adb = shutil.which("adb") is not None
|
||
|
||
if has_adb and u2:
|
||
try:
|
||
d = u2.connect(self.device_id)
|
||
d.implicitly_wait(10.0)
|
||
d.settings['operation_delay'] = (0, 0)
|
||
d.settings['operation_delay_methods'] = []
|
||
self.d = d
|
||
self._connection_guard = None
|
||
logger.info(f"🔄 u2 重连成功: {d.info.get('productName', 'Unknown')}")
|
||
return
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
from local_device import LocalDevice
|
||
d = LocalDevice()
|
||
self.d = d
|
||
self._connection_guard = None
|
||
logger.info("🔄 LocalDevice 重连成功")
|
||
except Exception as e:
|
||
logger.debug(f"设备重连尝试失败: {e}")
|
||
|
||
def _get_connection_guard(self):
|
||
"""获取或创建连接守护技能实例"""
|
||
if not hasattr(self, '_connection_guard'):
|
||
self._connection_guard = None
|
||
if self._connection_guard is None and self.d:
|
||
try:
|
||
from skills.connection_guard import ConnectionGuardSkill
|
||
self._connection_guard = ConnectionGuardSkill(self.d, bus=None)
|
||
logger.info("🛡️ 连接守护技能已加载")
|
||
except Exception as e:
|
||
logger.debug(f"连接守护加载失败: {e}")
|
||
return self._connection_guard
|
||
|
||
async def _connection_guard_loop(self):
|
||
"""
|
||
常驻连接守护循环 — 独立于心跳,高频扫描系统弹窗
|
||
|
||
与心跳的分工:
|
||
- 本循环:每 5 秒扫描一次弹窗(USB授权、权限请求等),确保不会因弹窗阻塞设备
|
||
- 心跳循环:每 N 次心跳做一次完整守护(包含网络/u2/屏幕等重量级检查)
|
||
|
||
当 u2 不可用时,会每 30 秒尝试重新初始化,而不是立即退出
|
||
(立即退出会导致 asyncio.wait(FIRST_COMPLETED) 杀掉所有任务→断连循环)
|
||
"""
|
||
scan_count = 0
|
||
u2_retry_interval = 30
|
||
|
||
while self.connected:
|
||
try:
|
||
guard = self._get_connection_guard()
|
||
|
||
if not guard:
|
||
if not self.d:
|
||
self._try_reconnect_u2()
|
||
guard = self._get_connection_guard()
|
||
|
||
if not guard:
|
||
await asyncio.sleep(u2_retry_interval)
|
||
continue
|
||
|
||
await asyncio.sleep(5)
|
||
if not self.connected:
|
||
break
|
||
|
||
scan_count += 1
|
||
result = await asyncio.get_event_loop().run_in_executor(
|
||
None, guard.dismiss_popups
|
||
)
|
||
if result.get("count", 0) > 0:
|
||
logger.info(f"🛡️ 守护扫描 #{scan_count}: 处理了 {result['count']} 个弹窗")
|
||
if self.ws and self.connected:
|
||
await self._send_event("popup_dismissed", {
|
||
"dismissed": result.get("dismissed", []),
|
||
"scan_count": scan_count,
|
||
})
|
||
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception as e:
|
||
logger.debug(f"守护扫描异常: {e}")
|
||
await asyncio.sleep(10)
|
||
|
||
logger.info("🛡️ 连接守护循环已停止")
|
||
|
||
async def _status_report_loop(self):
|
||
"""定期上报详细设备状态(每5分钟)"""
|
||
while self.connected:
|
||
try:
|
||
await asyncio.sleep(300) # 5分钟
|
||
|
||
if not self.connected or not self.ws:
|
||
break
|
||
|
||
status = self._get_full_status()
|
||
await self.ws.send(json.dumps({
|
||
"type": "status_report",
|
||
"data": status
|
||
}))
|
||
|
||
logger.debug(f"📊 状态上报完成")
|
||
|
||
except Exception:
|
||
break
|
||
|
||
def _get_quick_status(self) -> dict:
|
||
"""获取快速状态摘要(心跳用,低开销)"""
|
||
status = {"online": True}
|
||
|
||
if self.d:
|
||
try:
|
||
info = self.d.info
|
||
status["screen_on"] = info.get("screenOn", False)
|
||
status["current_app"] = info.get("currentPackageName", "")
|
||
|
||
wechat_pkg = "com.tencent.mm"
|
||
try:
|
||
running = self.d.shell(f"pidof {wechat_pkg}").output.strip()
|
||
status["wechat_running"] = bool(running)
|
||
status["wechat_pid"] = running if running else None
|
||
except Exception:
|
||
status["wechat_running"] = False
|
||
|
||
status["wechat_foreground"] = (
|
||
status.get("current_app") == wechat_pkg
|
||
)
|
||
|
||
try:
|
||
net_out = self.d.shell(
|
||
"dumpsys connectivity | grep -m1 'type: WIFI\\|type: MOBILE' || echo 'none'"
|
||
).output.strip()
|
||
status["network_type"] = (
|
||
"wifi" if "WIFI" in net_out
|
||
else "mobile" if "MOBILE" in net_out
|
||
else "none"
|
||
)
|
||
except Exception:
|
||
status["network_type"] = "unknown"
|
||
|
||
# 电量(轻量级,心跳附带)
|
||
try:
|
||
battery_out = self.d.shell("dumpsys battery | grep level").output.strip()
|
||
if "level:" in battery_out:
|
||
status["battery"] = int(battery_out.split(":")[1].strip())
|
||
except Exception:
|
||
pass
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
status["guard_active"] = hasattr(self, '_connection_guard') and self._connection_guard is not None
|
||
|
||
return status
|
||
|
||
def _get_full_status(self) -> dict:
|
||
"""获取完整设备状态"""
|
||
status = {
|
||
"device_id": self.device_id,
|
||
"agent_version": self.VERSION,
|
||
"uptime": int(time.time() - self.start_time) if self.start_time else 0,
|
||
"commands_executed": self.commands_executed,
|
||
"connected": self.connected,
|
||
"timestamp": datetime.now().isoformat(),
|
||
}
|
||
|
||
if self.d:
|
||
try:
|
||
info = self.d.info
|
||
status.update({
|
||
"screen_on": info.get("screenOn", False),
|
||
"current_app": info.get("currentPackageName", ""),
|
||
"display": {
|
||
"width": info.get("displayWidth", 0),
|
||
"height": info.get("displayHeight", 0),
|
||
},
|
||
"rotation": info.get("displayRotation", 0),
|
||
})
|
||
|
||
# 获取电池信息
|
||
try:
|
||
battery = self.d.shell("dumpsys battery | grep -E 'level|status|plugged'").output
|
||
for line in battery.strip().split('\n'):
|
||
line = line.strip()
|
||
if 'level:' in line:
|
||
status["battery_level"] = int(line.split(':')[1].strip())
|
||
elif 'status:' in line:
|
||
status["battery_status"] = int(line.split(':')[1].strip())
|
||
elif 'plugged:' in line:
|
||
status["charging"] = int(line.split(':')[1].strip()) > 0
|
||
except:
|
||
pass
|
||
|
||
# 获取网络信息
|
||
try:
|
||
wifi = self.d.shell("dumpsys wifi | grep 'Wi-Fi is'").output.strip()
|
||
status["wifi"] = "enabled" in wifi.lower()
|
||
ssid_out = self.d.shell(
|
||
"dumpsys wifi | grep -m1 'mWifiInfo' || echo ''"
|
||
).output.strip()
|
||
if "SSID:" in ssid_out:
|
||
import re
|
||
m = re.search(r'SSID:\s*([^,]+)', ssid_out)
|
||
if m:
|
||
status["wifi_ssid"] = m.group(1).strip().strip('"')
|
||
ip_out = self.d.shell("ip route | grep -m1 'src'").output.strip()
|
||
if "src " in ip_out:
|
||
status["ip_address"] = ip_out.split("src ")[-1].split()[0]
|
||
except Exception:
|
||
pass
|
||
|
||
# 微信详细状态
|
||
wechat_pkg = "com.tencent.mm"
|
||
try:
|
||
pid = self.d.shell(f"pidof {wechat_pkg}").output.strip()
|
||
status["wechat"] = {
|
||
"installed": True,
|
||
"running": bool(pid),
|
||
"pid": pid or None,
|
||
"foreground": info.get("currentPackageName") == wechat_pkg,
|
||
}
|
||
if pid:
|
||
mem = self.d.shell(
|
||
f"dumpsys meminfo {wechat_pkg} | grep 'TOTAL PSS' || echo ''"
|
||
).output.strip()
|
||
if "TOTAL" in mem:
|
||
parts = mem.split()
|
||
for p in parts:
|
||
if p.replace(",", "").isdigit():
|
||
status["wechat"]["memory_kb"] = int(p.replace(",", ""))
|
||
break
|
||
except Exception:
|
||
status["wechat"] = {"installed": False, "running": False}
|
||
|
||
except Exception as e:
|
||
status["error"] = str(e)
|
||
|
||
return status
|
||
|
||
# ====================================================================
|
||
# 三、消息处理(接收服务器命令,执行操作)
|
||
# ====================================================================
|
||
|
||
async def _message_loop(self):
|
||
"""消息接收循环(非阻塞:长耗时命令用 create_task 并发执行,不阻塞心跳 ACK 接收)"""
|
||
try:
|
||
async for message in self.ws:
|
||
data = json.loads(message)
|
||
msg_type = data.get("type")
|
||
if msg_type in ("pong", "heartbeat_ack"):
|
||
self.last_heartbeat_ack = time.time()
|
||
continue
|
||
if msg_type == "registered":
|
||
logger.info("✅ 服务器确认注册")
|
||
continue
|
||
asyncio.create_task(self._handle_message(message))
|
||
except websockets.ConnectionClosed:
|
||
logger.info("消息循环: 连接已关闭")
|
||
except Exception as e:
|
||
logger.error(f"消息循环错误: {e}")
|
||
|
||
async def _handle_message(self, message: str):
|
||
"""处理服务器消息(心跳/注册已在 _message_loop 快速路径处理)"""
|
||
try:
|
||
data = json.loads(message)
|
||
msg_type = data.get("type")
|
||
command_id = data.get("command_id")
|
||
|
||
logger.info(f"📩 收到命令: {msg_type} (id={command_id})")
|
||
|
||
if msg_type == "execute":
|
||
cmd_data = dict(data.get("data") or {})
|
||
# 兼容 ws_hub Android 扁平协议:action/params/script 在顶层
|
||
for _k in ("script", "action", "params", "channel", "hook_only"):
|
||
if _k not in cmd_data and _k in data:
|
||
cmd_data[_k] = data[_k]
|
||
result = await self._execute_command(cmd_data)
|
||
self.commands_executed += 1
|
||
await self._send_response(command_id, result)
|
||
# 技能执行完毕后上报事件,供服务端落库/转发
|
||
if cmd_data.get("script"):
|
||
await self._send_event("skill_done", {
|
||
"script": cmd_data.get("script"),
|
||
"action": cmd_data.get("action"),
|
||
"code": result.get("code", 200),
|
||
"success": result.get("code") == 200,
|
||
})
|
||
|
||
elif msg_type == "agent_execute":
|
||
result = await self._execute_agent_task(data.get("data", {}))
|
||
self.commands_executed += 1
|
||
await self._send_response(command_id, result)
|
||
|
||
elif msg_type == "ai_task":
|
||
if self.ai_brain:
|
||
task_data = data.get("data", {})
|
||
instruction = task_data.get("instruction", "")
|
||
priority = task_data.get("priority", 5)
|
||
self.ai_brain.add_task(instruction, source="server", priority=priority)
|
||
await self._send_response(command_id, {"code": 200, "message": "AI任务已入队"})
|
||
else:
|
||
await self._send_response(command_id, {"code": 503, "message": "AI Brain未启用"})
|
||
|
||
elif msg_type == "standing_order":
|
||
if self.ai_brain:
|
||
order = data.get("data", {}).get("order", "")
|
||
self.ai_brain.add_standing_order(order)
|
||
await self._send_response(command_id, {"code": 200, "message": "常驻指令已添加"})
|
||
|
||
elif msg_type == "config_update":
|
||
await self._handle_config_update(data.get("data", {}))
|
||
|
||
elif msg_type == "device_request_ack":
|
||
ack_data = data.get("data") or {}
|
||
if ack_data.get("heartbeat_interval") is not None:
|
||
self.heartbeat_interval = int(ack_data["heartbeat_interval"])
|
||
logger.info(f"已应用服务端配置: heartbeat_interval={self.heartbeat_interval}")
|
||
logger.debug(f"device_request_ack: request_id={data.get('request_id')} success={data.get('success')}")
|
||
|
||
elif msg_type == "agent_status":
|
||
status_info = {
|
||
"agent_version": self.version if hasattr(self, 'version') else "3.1.0",
|
||
"device_id": self.device_id,
|
||
"connected": self.connected,
|
||
"uptime": int(time.time() - self.start_time) if hasattr(self, 'start_time') else 0,
|
||
"ai_brain": bool(self.ai_brain),
|
||
"frida": bool(getattr(self, 'frida_mgr', None)),
|
||
"skills": list(self.skill_registry.keys()) if hasattr(self, 'skill_registry') else [],
|
||
}
|
||
await self._send_response(command_id, {"code": 200, "data": status_info})
|
||
|
||
else:
|
||
logger.warning(f"未知消息类型: {msg_type}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理消息错误: {e}")
|
||
if command_id:
|
||
await self._send_response(command_id, {
|
||
"code": 500,
|
||
"message": str(e)
|
||
})
|
||
|
||
async def _send_response(self, command_id: str, result: dict):
|
||
"""发送命令执行结果"""
|
||
if not self.ws or not self.connected:
|
||
return
|
||
|
||
try:
|
||
await self.ws.send(json.dumps({
|
||
"type": "response",
|
||
"command_id": command_id,
|
||
"device_id": self.device_id,
|
||
"code": result.get("code", 200),
|
||
"message": result.get("message", "success"),
|
||
"data": result.get("data", {}),
|
||
"timestamp": int(time.time()),
|
||
}))
|
||
except Exception as e:
|
||
logger.error(f"发送响应失败: {e}")
|
||
|
||
async def _send_event(self, event: str, data: dict = None):
|
||
"""设备端事件上报(技能执行完毕、异常等),由服务端处理/落库/转发"""
|
||
if not self.ws or not self.connected:
|
||
return
|
||
try:
|
||
await self.ws.send(json.dumps({
|
||
"type": "event",
|
||
"device_id": self.device_id,
|
||
"event": event,
|
||
"data": data or {},
|
||
"timestamp": int(time.time()),
|
||
}))
|
||
except Exception as e:
|
||
logger.error(f"发送事件失败: {e}")
|
||
|
||
async def _send_device_request(self, action: str, params: dict = None) -> dict:
|
||
"""设备端请求服务端执行操作(拉配置、落库等),等待 device_request_ack(可选)"""
|
||
if not self.ws or not self.connected:
|
||
return {"success": False, "error": "未连接"}
|
||
request_id = f"req_{int(time.time() * 1000)}_{random.randint(1000, 9999)}"
|
||
try:
|
||
await self.ws.send(json.dumps({
|
||
"type": "device_request",
|
||
"device_id": self.device_id,
|
||
"request_id": request_id,
|
||
"action": action,
|
||
"params": params or {},
|
||
"timestamp": int(time.time()),
|
||
}))
|
||
except Exception as e:
|
||
logger.error(f"发送 device_request 失败: {e}")
|
||
return {"success": False, "error": str(e)}
|
||
return {"success": True, "request_id": request_id}
|
||
|
||
async def _handle_config_update(self, config: dict):
|
||
"""处理服务器推送的配置更新"""
|
||
if "heartbeat_interval" in config:
|
||
self.heartbeat_interval = config["heartbeat_interval"]
|
||
logger.info(f"心跳间隔更新为: {self.heartbeat_interval}秒")
|
||
|
||
# ====================================================================
|
||
# 四、命令执行(操作手机)
|
||
# ====================================================================
|
||
|
||
async def _execute_command(self, cmd: dict) -> dict:
|
||
"""执行命令(Frida优先 → u2兜底),前置风控检查"""
|
||
action = cmd.get("action")
|
||
params = cmd.get("params", {})
|
||
script = cmd.get("script")
|
||
channel = cmd.get("channel", "auto") # auto | frida | u2
|
||
hook_only = bool(cmd.get("hook_only"))
|
||
|
||
# 风控哨兵前置检查
|
||
if self.risk_sentinel and action:
|
||
check_result = self.risk_sentinel.check(action)
|
||
if not check_result.get("allowed", True):
|
||
return {
|
||
"code": 429,
|
||
"message": f"风控限制: {check_result.get('reason', '操作频率过高')}",
|
||
"data": {"risk_check": check_result},
|
||
}
|
||
|
||
try:
|
||
if script:
|
||
if channel == "u2":
|
||
return await self._execute_skill(script, action, params)
|
||
return await self._execute_with_frida_priority(
|
||
script, action, params, hook_only=hook_only
|
||
)
|
||
|
||
if not self.d:
|
||
return {"code": 503, "message": "uiautomator2未连接"}
|
||
|
||
loop = asyncio.get_running_loop()
|
||
|
||
def _run_basic():
|
||
if action == "screenshot":
|
||
return self._screenshot()
|
||
elif action == "click":
|
||
self.d.click(params["x"], params["y"])
|
||
return {"code": 200, "data": {"success": True}}
|
||
elif action == "click_text":
|
||
text = params["text"]
|
||
t = params.get("timeout", 10)
|
||
if self.d(text=text).wait(timeout=t):
|
||
self.d(text=text).click()
|
||
return {"code": 200, "data": {"success": True, "text": text}}
|
||
return {"code": 404, "message": f"未找到: {text}"}
|
||
elif action == "input":
|
||
if params.get("clear", True):
|
||
self.d.clear_text()
|
||
self.d.send_keys(params["text"])
|
||
return {"code": 200, "data": {"success": True}}
|
||
elif action == "swipe":
|
||
if "x1" in params and "y1" in params and "x2" in params and "y2" in params:
|
||
dur = params.get("duration", 300) / 1000.0
|
||
self.d.swipe(params["x1"], params["y1"], params["x2"], params["y2"], duration=dur)
|
||
else:
|
||
direction = params.get("direction", "up")
|
||
self.d.swipe_ext(direction, scale=params.get("scale", 0.8))
|
||
return {"code": 200, "data": {"success": True}}
|
||
elif action == "ui_tree":
|
||
xml = self.d.dump_hierarchy()
|
||
return {"code": 200, "data": {"xml": xml, "length": len(xml)}}
|
||
elif action == "app_start":
|
||
self.d.app_start(params["package"])
|
||
return {"code": 200, "data": {"success": True}}
|
||
elif action == "app_stop":
|
||
self.d.app_stop(params["package"])
|
||
return {"code": 200, "data": {"success": True}}
|
||
elif action == "press_key":
|
||
self.d.press(params.get("key", "home"))
|
||
return {"code": 200, "data": {"success": True}}
|
||
elif action == "device_info":
|
||
return {"code": 200, "data": self._get_device_info()}
|
||
elif action == "status":
|
||
return {"code": 200, "data": self._get_full_status()}
|
||
else:
|
||
return {"code": 400, "message": f"未知操作: {action}"}
|
||
|
||
return await loop.run_in_executor(None, _run_basic)
|
||
|
||
except Exception as e:
|
||
logger.error(f"执行命令错误: {e}")
|
||
if self.risk_sentinel and action:
|
||
self.risk_sentinel.record(action)
|
||
return {"code": 500, "message": str(e)}
|
||
|
||
async def _execute_skill(self, script: str, action: str, params: dict) -> dict:
|
||
"""执行APP技能"""
|
||
try:
|
||
if not self.d:
|
||
return {"code": 503, "message": "uiautomator2未连接"}
|
||
|
||
# 动态加载技能
|
||
# 通过技能注册表获取
|
||
from skills import get_skill
|
||
try:
|
||
skill_class = get_skill(script)
|
||
except ImportError as ie:
|
||
return {"code": 404, "message": str(ie)}
|
||
|
||
anti_ban_ctx = {
|
||
"risk_sentinel": self.risk_sentinel,
|
||
"touch_hardener": self.touch_hardener,
|
||
"sensor_sim": self.sensor_sim,
|
||
"nurture_scheduler": self.nurture_scheduler,
|
||
}
|
||
skill = skill_class(self.d, anti_ban_ctx=anti_ban_ctx)
|
||
|
||
# 调用方法
|
||
method = getattr(skill, action, None)
|
||
if not method:
|
||
return {"code": 404, "message": f"技能{script}不支持操作: {action}"}
|
||
|
||
# 执行(同步方法放到线程池,避免阻塞事件循环导致 WS ping 超时断连)
|
||
import inspect
|
||
if inspect.iscoroutinefunction(method):
|
||
result = await method(**params)
|
||
else:
|
||
loop = asyncio.get_running_loop()
|
||
result = await loop.run_in_executor(None, lambda: method(**params))
|
||
|
||
return {"code": 200, "data": result}
|
||
|
||
except Exception as e:
|
||
logger.error(f"执行技能错误 [{script}.{action}]: {e}")
|
||
return {"code": 500, "message": str(e)}
|
||
|
||
async def _execute_agent_task(self, data: dict) -> dict:
|
||
"""执行AI Agent任务(自然语言控制);优先走 SkillExecutor 微信/抖音复合任务,无 LLM 也可执行"""
|
||
task = (data.get("task") or "").strip()
|
||
logger.info(f"🤖 AI任务: {task}")
|
||
if not task:
|
||
return {"code": 400, "message": "task 为空", "data": {"success": False, "error": "task 为空"}}
|
||
|
||
try:
|
||
if not self.d:
|
||
return {"code": 503, "message": "uiautomator2未连接", "data": {"success": False, "error": "设备未连接"}}
|
||
try:
|
||
from skill_executor import SkillExecutor
|
||
executor = SkillExecutor(self.d, anti_ban_ctx={
|
||
"risk_sentinel": self.risk_sentinel,
|
||
"touch_hardener": self.touch_hardener,
|
||
"sensor_sim": self.sensor_sim,
|
||
"nurture_scheduler": self.nurture_scheduler,
|
||
})
|
||
except ImportError:
|
||
executor = None
|
||
if executor:
|
||
result = None
|
||
if "微信" in task:
|
||
result = executor.execute_wechat_task(task)
|
||
elif "抖音" in task:
|
||
result = executor.execute_douyin_task(task)
|
||
elif "小红书" in task:
|
||
result = executor.execute_xhs_task(task)
|
||
elif "闲鱼" in task:
|
||
result = executor.execute_xianyu_task(task)
|
||
elif "Soul" in task or "soul" in task.lower() or "灵魂" in task:
|
||
result = executor.execute_soul_task(task)
|
||
elif any(k in task for k in ("连接网络", "恢复网络", "打开WiFi", "打开网络", "断网", "连不上网", "上网")):
|
||
result = executor.execute_network_reconnect()
|
||
else:
|
||
result = executor.execute_command(task)
|
||
if result is None:
|
||
result = {"success": False, "error": "未匹配到执行路径"}
|
||
return {"code": 200, "data": result}
|
||
return {"code": 200, "data": {"success": False, "error": "AI Agent任务引擎未加载", "task": task}}
|
||
except Exception as e:
|
||
logger.exception(f"AI任务执行异常: {e}")
|
||
return {"code": 500, "message": str(e), "data": {"success": False, "error": str(e), "task": task}}
|
||
|
||
# ====================================================================
|
||
# 五、设备信息
|
||
# ====================================================================
|
||
|
||
def _screenshot(self) -> dict:
|
||
"""截图"""
|
||
try:
|
||
img = self.d.screenshot(format='raw')
|
||
b64 = base64.b64encode(img).decode('utf-8')
|
||
info = self.d.info
|
||
return {
|
||
"code": 200,
|
||
"data": {
|
||
"base64": b64,
|
||
"width": info.get("displayWidth", 0),
|
||
"height": info.get("displayHeight", 0),
|
||
"size": len(img),
|
||
}
|
||
}
|
||
except Exception as e:
|
||
return {"code": 500, "message": str(e)}
|
||
|
||
def _get_device_info(self) -> dict:
|
||
"""获取设备信息"""
|
||
info = {}
|
||
|
||
if self.d:
|
||
try:
|
||
d_info = self.d.info
|
||
info = {
|
||
"device_id": self.device_id,
|
||
"model": d_info.get("productName", "Unknown"),
|
||
"brand": d_info.get("brand", "Unknown"),
|
||
"android_version": str(d_info.get("sdkVersion", "Unknown")),
|
||
"display": {
|
||
"width": d_info.get("displayWidth", 0),
|
||
"height": d_info.get("displayHeight", 0),
|
||
},
|
||
"screen_on": d_info.get("screenOn", False),
|
||
}
|
||
|
||
# 检测已安装的APP
|
||
apps = []
|
||
try:
|
||
output = self.d.shell("pm list packages -3").output
|
||
app_detect = {
|
||
'com.tencent.mm': 'wechat',
|
||
'com.ss.android.ugc.aweme': 'douyin',
|
||
'com.xingin.xhs': 'xhs',
|
||
'com.taobao.idlefish': 'xianyu',
|
||
'cn.soulapp.android': 'soul',
|
||
}
|
||
for line in output.strip().split('\n'):
|
||
pkg = line.replace('package:', '').strip()
|
||
for check_pkg, name in app_detect.items():
|
||
if check_pkg in pkg:
|
||
apps.append(name)
|
||
except:
|
||
pass
|
||
|
||
info["installed_apps"] = apps
|
||
capabilities = [
|
||
"u2", "screenshot", "click", "input", "swipe",
|
||
"ui_tree", "app_control", "skill_execute",
|
||
"skill_wechat", "skill_douyin", "skill_xhs", "skill_xianyu",
|
||
"skill_network_reconnect", "hawk",
|
||
"connection_guard", "popup_auto_dismiss",
|
||
"event", "device_request",
|
||
]
|
||
if self.frida_mgr and self.frida_mgr.connected:
|
||
capabilities.extend(["frida", "hook", "frida_rpc"])
|
||
if self.ai_brain:
|
||
capabilities.extend(["ai_brain", "autonomous_mode"])
|
||
info["capabilities"] = capabilities
|
||
|
||
if self.frida_mgr:
|
||
info["frida"] = self.frida_mgr.get_status()
|
||
if self.ai_brain:
|
||
info["ai_brain"] = self.ai_brain.get_status()
|
||
# 防护模块状态
|
||
info["anti_ban"] = {}
|
||
if self.device_guard:
|
||
info["anti_ban"]["guard"] = self.device_guard.report
|
||
if self.risk_sentinel:
|
||
info["anti_ban"]["sentinel"] = self.risk_sentinel.get_stats()
|
||
if self.nurture_scheduler:
|
||
info["anti_ban"]["nurture"] = self.nurture_scheduler.get_stats(self.device_id)
|
||
|
||
except Exception as e:
|
||
info["error"] = str(e)
|
||
else:
|
||
info = {
|
||
"device_id": self.device_id,
|
||
"model": "Unknown (u2未连接)",
|
||
"capabilities": [],
|
||
}
|
||
|
||
info["agent_version"] = self.VERSION
|
||
info["project_id"] = self.project_id
|
||
|
||
return info
|
||
|
||
# ====================================================================
|
||
# 六、启动/停止
|
||
# ====================================================================
|
||
|
||
async def start(self):
|
||
"""启动Agent"""
|
||
self.running = True
|
||
self.start_time = time.time()
|
||
|
||
logger.info("=" * 60)
|
||
logger.info(f"🚀 AI数字员工 v{self.VERSION} 启动(Frida + AI Brain + 连接守护 增强版)")
|
||
logger.info(f" 设备ID: {self.device_id}")
|
||
logger.info(f" 服务器: {self.server_url}")
|
||
logger.info(f" 项目ID: {self.project_id}")
|
||
logger.info(f" 心跳间隔: {self.heartbeat_interval}秒 | 弹窗守护: 5秒")
|
||
logger.info(f" Frida: {'✅' if self.frida_mgr and self.frida_mgr.connected else '❌'}")
|
||
logger.info(f" AI Brain: {'✅' if self.ai_brain else '⬜'}")
|
||
guard = self._get_connection_guard()
|
||
logger.info(f" 连接守护: {'✅' if guard else '⬜ (无设备)'}")
|
||
logger.info("=" * 60)
|
||
|
||
await self.connect()
|
||
|
||
async def stop_async(self):
|
||
"""异步停止Agent(清理资源)"""
|
||
self.running = False
|
||
self.connected = False
|
||
self._stop_autonomous_mode()
|
||
if self.frida_mgr:
|
||
self.frida_mgr.stop()
|
||
if self.ai_brain:
|
||
await self.ai_brain.close()
|
||
logger.info("🛑 AI数字员工已停止")
|
||
|
||
def stop(self):
|
||
"""停止Agent"""
|
||
self.running = False
|
||
self.connected = False
|
||
self._stop_autonomous_mode()
|
||
if self.frida_mgr:
|
||
self.frida_mgr.stop()
|
||
logger.info("🛑 AI数字员工已停止")
|
||
|
||
|
||
def _detect_device_id() -> str:
|
||
"""自动检测设备ID(Termux / ADB / fallback)"""
|
||
import subprocess
|
||
# 1. Termux: getprop
|
||
try:
|
||
r = subprocess.run(['getprop', 'ro.serialno'], capture_output=True, text=True, timeout=5)
|
||
serial = r.stdout.strip()
|
||
if serial:
|
||
return serial
|
||
except Exception:
|
||
pass
|
||
# 2. ADB serial(从环境变量,模拟器常用)
|
||
serial = os.environ.get("ANDROID_SERIAL", "")
|
||
if serial:
|
||
return serial
|
||
# 3. 通过 uiautomator2 获取
|
||
if u2:
|
||
try:
|
||
d = u2.connect()
|
||
serial = d.serial
|
||
if serial:
|
||
return serial
|
||
except Exception:
|
||
pass
|
||
# 4. fallback: 基于时间戳
|
||
return f"agent-{int(time.time())}"
|
||
|
||
|
||
def _resolve_config(args) -> dict:
|
||
"""
|
||
配置级联解析(优先级: 环境变量 > 命令行 > config.json > 默认值)
|
||
返回 {device_id, server_url, heartbeat_interval, project_id, ai_config}
|
||
"""
|
||
config = {}
|
||
config_path = args.config or os.path.join(_AGENT_DIR, 'config.json')
|
||
if os.path.exists(config_path):
|
||
try:
|
||
with open(config_path) as f:
|
||
config = json.load(f)
|
||
logger.info(f"📄 已加载配置: {config_path}")
|
||
except Exception as e:
|
||
logger.warning(f"读取配置文件失败: {e}")
|
||
|
||
device_id = (
|
||
os.environ.get("WP_DEVICE_ID")
|
||
or args.device_id
|
||
or config.get("device_id")
|
||
or _detect_device_id()
|
||
)
|
||
server_base = (
|
||
os.environ.get("WP_SERVER_URL")
|
||
or args.server
|
||
or config.get("server_url")
|
||
or "ws://192.168.1.100:8899/ws/device"
|
||
).rstrip("/")
|
||
heartbeat = int(
|
||
os.environ.get("WP_HEARTBEAT")
|
||
or (args.heartbeat if args.heartbeat is not None else 0)
|
||
or config.get("heartbeat_interval")
|
||
or 10
|
||
)
|
||
project_id = (
|
||
os.environ.get("WP_PROJECT_ID")
|
||
or args.project
|
||
or config.get("project_id")
|
||
or "cunkebao"
|
||
)
|
||
|
||
server_url = f"{server_base}/{device_id}"
|
||
|
||
# AI Brain 配置(环境变量 > config.json.ai_brain)
|
||
ai_cfg = config.get("ai_brain", {})
|
||
ai_config = {
|
||
"enabled": (os.environ.get("WP_AI_ENABLED", "").lower() in ("1", "true")
|
||
or ai_cfg.get("enabled", False)),
|
||
"api_url": os.environ.get("WP_AI_API_URL") or ai_cfg.get("api_url", "http://localhost:3102"),
|
||
"api_key": os.environ.get("WP_AI_API_KEY") or ai_cfg.get("api_key", ""),
|
||
"model": os.environ.get("WP_AI_MODEL") or ai_cfg.get("model", "auto"),
|
||
"brain_interval": int(os.environ.get("WP_AI_INTERVAL", "0")
|
||
or ai_cfg.get("brain_interval", 60)),
|
||
"standing_orders": ai_cfg.get("standing_orders", []),
|
||
}
|
||
|
||
return {
|
||
"device_id": device_id,
|
||
"server_url": server_url,
|
||
"heartbeat_interval": heartbeat,
|
||
"project_id": project_id,
|
||
"ai_config": ai_config,
|
||
}
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description='AI数字员工 - 工作手机Agent v3.1(Frida + AI Brain 增强版)',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
配置优先级: 环境变量 > 命令行参数 > config.json > 默认值
|
||
|
||
环境变量:
|
||
WP_DEVICE_ID 设备ID(默认自动检测)
|
||
WP_SERVER_URL WebSocket基础地址
|
||
WP_PROJECT_ID 项目ID
|
||
WP_HEARTBEAT 心跳间隔(秒)
|
||
WP_AI_ENABLED 启用AI Brain(1/true)
|
||
WP_AI_API_URL 卡若AI API地址
|
||
WP_AI_API_KEY 卡若AI API密钥
|
||
WP_AI_INTERVAL AI思考间隔(秒)
|
||
|
||
示例:
|
||
# 基本启动(读 config.json)
|
||
python agent.py
|
||
|
||
# 启用 AI Brain
|
||
WP_AI_ENABLED=1 WP_AI_API_KEY=xxx python agent.py
|
||
|
||
# 指定服务器 + AI
|
||
python agent.py -s ws://192.168.1.100:8899/ws/device
|
||
"""
|
||
)
|
||
parser.add_argument('--device-id', '-d', default=None, help='设备ID(默认自动检测)')
|
||
parser.add_argument('--server', '-s', default=None, help='服务器WebSocket基础地址')
|
||
parser.add_argument('--heartbeat', '-hb', type=int, default=None, help='心跳间隔(秒),建议5/10/30')
|
||
parser.add_argument('--project', '-p', default=None, help='项目ID')
|
||
parser.add_argument('--config', '-c', default=None, help='配置文件路径(默认 config.json)')
|
||
|
||
args = parser.parse_args()
|
||
cfg = _resolve_config(args)
|
||
|
||
agent = WorkPhoneAgent(
|
||
device_id=cfg["device_id"],
|
||
server_url=cfg["server_url"],
|
||
heartbeat_interval=cfg["heartbeat_interval"],
|
||
project_id=cfg["project_id"],
|
||
ai_config=cfg.get("ai_config"),
|
||
)
|
||
|
||
# 信号处理:优雅关闭
|
||
def _signal_handler(sig, frame):
|
||
logger.info(f"收到信号 {sig},正在停止...")
|
||
agent.stop()
|
||
|
||
signal.signal(signal.SIGINT, _signal_handler)
|
||
signal.signal(signal.SIGTERM, _signal_handler)
|
||
|
||
try:
|
||
asyncio.run(agent.start())
|
||
except KeyboardInterrupt:
|
||
pass
|
||
finally:
|
||
agent.stop()
|
||
logger.info("👋 Agent已退出")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|