Files
workphone-sdk/sdk/app/agent/agent.py
卡若 9118df9eee
Some checks failed
SDK CI / python-compile (push) Has been cancelled
feat: sdk agent、Android 与文档同步至 Gitea
- 新增 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>
2026-05-06 18:20:14 +08:00

1490 lines
61 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.

#!/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数字员工 - 手机端AgentFrida + 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 不可用 → LocalDeviceATX 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 RPCHook 级别,直接操作 APP 内部)
2. Frida 不可用时降级到 u2UI自动化
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:
"""自动检测设备IDTermux / 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.1Frida + 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 Brain1/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()