#!/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
import subprocess
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
from datetime import datetime
from typing import Optional, Dict, Any, Callable, List

# 确保 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,
        public_servers: Optional[list] = None,
    ):
        self.device_id = device_id
        # 传输序列：u2/frida 连接所用的 adb 目标，可与逻辑 device_id 分离。
        # adb-over-WiFi 无 USB 主控时设 WP_DEVICE_SERIAL=ip:5555（如 192.168.110.80:5555），
        # WS 身份仍用 device_id（保持 SDK/hub 设备身份不变），u2/frida 走 WiFi adb。
        self.adb_serial = os.environ.get("WP_DEVICE_SERIAL", "").strip() or device_id
        self.server_url = server_url
        self.heartbeat_interval = heartbeat_interval
        self.project_id = project_id

        # BIND-03 公网主服有序回退：主连接 + 公网候选（断 LAN 后逐个健康探测）
        self.public_servers = [s for s in (public_servers or []) if s]
        self.server_candidates = self._build_server_candidates(server_url, self.public_servers, device_id)
        self._candidate_idx = 0
        # BIND-07 当前寻服阶段（lan/primary/public/retry），供日志与上报
        self.connect_stage = "primary"

        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
        self._loop: Optional[asyncio.AbstractEventLoop] = None

        # u2 非线程安全：命令与连接守护必须串行，否则长任务期间 ATX 挂死 → WS 断连
        self._u2_lock = asyncio.Lock()
        self._u2_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="wp-u2")
        self._cmd_semaphore = asyncio.Semaphore(1)

        # 设备控制通道（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 _running_on_device(self) -> bool:
        """True = Termux 等手机本机运行；False = Mac/PC 通过 ADB 控制"""
        import os
        if os.environ.get("WP_AGENT_ON_DEVICE", "").strip().lower() in ("1", "true", "yes"):
            return True
        return os.path.exists("/data/data/com.termux/files/usr/bin")

    def _adb_serial_candidates(self) -> List[str]:
        """优先用显式指定串口；掉线时自动回退逻辑 device_id（通常是 USB serial）。"""
        serials: List[str] = []
        for serial in (self.adb_serial, self.device_id):
            serial = (serial or "").strip()
            if serial and serial not in serials:
                serials.append(serial)
        return serials

    def _connect_u2_with_fallback(self):
        """u2 连接：优先 WP_DEVICE_SERIAL，失败则回退 device_id。"""
        last_error = None
        for serial in self._adb_serial_candidates():
            try:
                d = u2.connect(serial)
                d.implicitly_wait(10.0)
                d.settings['operation_delay'] = (0, 0)
                d.settings['operation_delay_methods'] = []
                if serial != self.adb_serial:
                    logger.info(f"u2 串口自动回退: {self.adb_serial} -> {serial}")
                self.adb_serial = serial
                return d
            except Exception as e:
                last_error = e
        raise last_error or RuntimeError("u2 connect failed")

    def _init_u2(self):
        """
        初始化设备控制（优先 u2，降级到 LocalDevice）

        连接策略：
        1. 有 ADB 环境 → u2.connect(serial)
        2. 仅手机本机（Termux）→ LocalDevice（ATX HTTP 直连）
        3. Mac/PC 无 ADB → self.d 保持 None，守护循环定期重试 u2
        """
        import shutil
        has_adb = shutil.which("adb") is not None

        if has_adb and u2:
            try:
                self.d = self._connect_u2_with_fallback()
                logger.info(f"u2 通过 ADB 连接({self.adb_serial}): {self.d.info.get('productName', 'Unknown')}")
                return
            except Exception as e:
                logger.info(f"u2 ADB 连接失败: {e}")
                if not self._running_on_device():
                    logger.warning(
                        "Mac/PC Agent：等待 ADB 设备（请 USB 授权或 adb connect），不降级 LocalDevice"
                    )
                    return

        if not self._running_on_device():
            return

        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
            import json as _json

            _mode = (_os.environ.get("WP_FRIDA_MODE") or "").strip().lower()
            _serial = self.adb_serial  # = WP_DEVICE_SERIAL or device_id（adb-over-WiFi 时为 ip:5555）
            _frida_port = int(_os.environ.get("WP_FRIDA_PORT") or "0")

            # 自动读取 phantom frida-server 配置（反检测随机端口）
            _on_device = self._running_on_device()
            _use_adb_forward = not _on_device
            if not _mode or _mode == "auto":
                _mode = "remote" if _on_device else "gadget"
                _cfg_candidates = [
                    _os.path.join(_os.path.dirname(__file__), "..", "..", "scripts", "anti_detect", "phantom_frida_config.json"),
                    _os.path.join(_os.path.dirname(__file__), "..", "scripts", "anti_detect", "phantom_frida_config.json"),
                    "/Users/karuo/Documents/开发/2、私域银行/工作手机/sdk/scripts/anti_detect/phantom_frida_config.json",
                    "/data/data/com.termux/files/home/workphone/phantom_frida_config.json",
                    "/sdcard/workphone/phantom_frida_config.json",
                ]
                for _cfg_path in _cfg_candidates:
                    _cfg_path = _os.path.abspath(_cfg_path)
                    if not _os.path.isfile(_cfg_path):
                        continue
                    try:
                        with open(_cfg_path, "r", encoding="utf-8") as _f:
                            _pc = _json.load(_f)
                        _ps = (_pc.get("device_serial") or "").strip()
                        if _ps and (_ps == self.adb_serial or not _serial):
                            _serial = _ps or _serial
                        if _pc.get("listen_port"):
                            _frida_port = int(_pc["listen_port"])
                            _mode = "remote"
                            logger.info(f"已加载 Phantom Frida 配置: port={_frida_port} serial={_serial}")
                        break
                    except Exception as _e:
                        logger.debug(f"读取 phantom 配置失败 {_cfg_path}: {_e}")

            if _on_device and _mode in ("gadget", "remote"):
                _mode = "remote"
                if not _frida_port:
                    _frida_port = 10431

            if _mode not in ("usb", "gadget", "remote"):
                _mode = "remote" if _on_device else "gadget"
            if not _serial and _mode == "usb":
                _serial = self.adb_serial

            self.frida_mgr = FridaManager(
                device_serial=_serial,
                mode=_mode,
                gadget_port=_frida_port if _frida_port else 0,
                gadget_host="127.0.0.1",
                use_adb_forward=_use_adb_forward,
                on_event=self._on_frida_event,
                auto_reconnect=True,
            )
            startup_timeout = float(_os.environ.get("WP_FRIDA_START_TIMEOUT", "12") or "12")
            logger.info(f"Frida 启动尝试: mode={_mode} serial={_serial} port={_frida_port or self.frida_mgr.gadget_port} timeout={startup_timeout}s")
            executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="wp-frida-start")
            future = executor.submit(self.frida_mgr.start)
            try:
                started = future.result(timeout=startup_timeout)
            except FutureTimeoutError:
                logger.warning(f"⚠️ Frida 启动超过 {startup_timeout:.0f}s，先降级到 u2/ADB，避免 Agent WS 离线")
                started = False
            finally:
                executor.shutdown(wait=False, cancel_futures=True)
            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]}")
        loop = self._loop
        if self.connected and self.ws and loop and loop.is_running():
            loop.call_soon_threadsafe(
                lambda et=event_type, pl=payload: asyncio.create_task(
                    self._send_event(f"hook_{et}", pl)
                )
            )

    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
    
    # ====================================================================
    # 一、连接管理（核心：主动连接 + 心跳保活 + 指数退避重连）
    # ====================================================================
    
    @staticmethod
    def _build_server_candidates(primary_url: str, public_servers: list, device_id: str) -> list:
        """BIND-03：构建有序候选 WS 列表 [主连接, 公网1, 公网2, ...]，去重。
        public_servers 元素可为含/不含 /ws/device 的基址，自动补全 + 拼 device_id。"""
        def _full(u: str) -> str:
            u = u.strip().rstrip("/")
            if not u:
                return ""
            # 已经带 device_id（结尾段不是 ws/device 关键字）则原样
            if u.endswith(f"/{device_id}"):
                return u
            if "/ws/device" not in u:
                u = f"{u}/ws/device"
            return f"{u}/{device_id}"

        candidates = []
        for u in [primary_url] + list(public_servers or []):
            full = _full(u)
            if full and full not in candidates:
                candidates.append(full)
        return candidates or [primary_url]

    @staticmethod
    def _probe_ws_base(ws_url: str, timeout: float = 3.0) -> bool:
        """BIND-03 健康探测：对候选 WS 的 host:port 做一次 TCP 连接，可达才尝试 WS 握手。"""
        import socket
        from urllib.parse import urlparse
        try:
            p = urlparse(ws_url)
            host = p.hostname
            port = p.port or (443 if p.scheme == "wss" else 8899)
            if not host:
                return False
            with socket.create_connection((host, port), timeout=timeout):
                return True
        except Exception:
            return False

    def _select_reachable_candidate(self) -> Optional[str]:
        """从当前候选起，按序探测，返回第一个 TCP 可达的候选并更新阶段标记。"""
        n = len(self.server_candidates)
        if n == 0:
            return self.server_url
        for offset in range(n):
            idx = (self._candidate_idx + offset) % n
            cand = self.server_candidates[idx]
            stage = "primary" if idx == 0 else "public"
            if self._probe_ws_base(cand):
                self._candidate_idx = idx
                self.connect_stage = stage
                if idx != 0:
                    logger.info(f"🌐 [寻服:公网回退] 候选 {idx+1}/{n} 可达 → {cand}")
                return cand
            logger.info(f"🔎 [寻服:探测] 候选 {idx+1}/{n} 不可达 → {cand}")
        # 全部不可达：返回主连接让 websockets 自行报错并触发重连/重发现
        self._candidate_idx = 0
        self.connect_stage = "retry"
        return self.server_candidates[0]

    async def connect(self):
        """主连接循环 - 永不停止，断连时切换离线AI自主模式"""
        while self.running:
            try:
                # BIND-03/07：按候选有序探测，选第一个可达；全不可达则回主连接重试
                if len(self.server_candidates) > 1:
                    chosen = self._select_reachable_candidate()
                    if chosen:
                        self.server_url = chosen
                logger.info(f"📡 [寻服:{self.connect_stage}] 正在连接服务器: {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

                # BIND-03/07：连接失败 → 轮换到下一个候选服务器（公网回退链）
                if len(self.server_candidates) > 1:
                    self._candidate_idx = (self._candidate_idx + 1) % len(self.server_candidates)
                    nxt = self.server_candidates[self._candidate_idx]
                    self.connect_stage = "primary" if self._candidate_idx == 0 else "public"
                    logger.info(f"🔁 [寻服:{self.connect_stage}] 切换候选 {self._candidate_idx+1}/{len(self.server_candidates)} → {nxt}")

                # 断连后启动离线自主模式（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 _run_u2_sync(self, fn: Callable[[], Any]) -> Any:
        """单线程池 + 互斥锁执行 u2 同步 I/O，保证长任务期间连接守护仍可排队而不并发打挂 ATX。"""
        async with self._u2_lock:
            loop = asyncio.get_running_loop()
            return await loop.run_in_executor(self._u2_executor, fn)

    async def _run_frida_sync(self, fn: Callable[[], Any]) -> Any:
        """Frida RPC/reload 放线程池，避免阻塞 asyncio 导致 WS 心跳发不出被 sweeper 踢线。"""
        async with self._u2_lock:
            loop = asyncio.get_running_loop()
            return await loop.run_in_executor(self._u2_executor, fn)
    
    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)

    U2_FORCE_ACTIONS = frozenset({
        "login_by_password",
        "ensure_logged_in",
        "unblock_via_customer_service",
        "unblock_account",
        "unblock_self",
        "unblock_appeal",
        "unblock_with_sms",
        "set_nickname",
        "set_signature",
        "set_avatar",
        "set_sex",
        "set_region",
        "set_what_up",
        "change_password",
        "bind_phone",
        "unbind_phone",
        "get_login_devices",
        "remove_login_device",
        "enable_fingerprint",
        "set_account_protection",
        "send_red_packet",
        "receive_red_packet",
        "send_transfer",
        "receive_transfer",
        "show_payment_code",
        "receive_payment",
        "get_wallet_balance",
        "get_transaction_history",
        "generate_my_qr_code",
        "generate_group_qr_code",
        "scan_qr_code",
        "add_friend_by_qr",
        "extract_qr_from_image",
        "add_friend_from_image",
        "voice_call",
        "video_call",
        "share_real_time_location",
        "download_file",
        "mass_send",
        "batch_send",
        "add_favorite",
        "delete_favorite",
        "follow_official_account",
        "unfollow_official_account",
        "open_mini_program",
        "get_recent_mini_programs",
        "share_mini_program",
        "send_voice",
        "send_file",
        "send_location",
        "send_card",
        "send_emoji",
        "add_custom_emoji",
        "add_to_float",
        "remove_from_float",
        "set_privacy",
        "set_notification",
        "clear_chat_history",
        "set_chat_background",
        "set_do_not_disturb",
        "pin_chat",
        "clear_cache",
        "check_for_update",
        "delete_moments",
        "browse_channels",
        "like_channel_video",
        "comment_channel_video",
        "follow_channel",
        "unfollow_channel",
        "share_channel_video",
        # forward_message / forward_multiple / revoke_message 已实现真 Frida RPC
        # （DB 取原文复用 _sendMessageInternal / 撤回资格预检），不再强制 u2，走 Frida 主通道
        "get_official_account_articles",
        "reply_comment",
    })

    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 联调）
        """
        if script == "wechat" and action in self.U2_FORCE_ACTIONS:
            if not self.d:
                self._try_reconnect_u2()
            if not self.d:
                return {
                    "code": 503,
                    "message": "uiautomator2未连接，请确认 USB 调试已授权且 adb devices 可见",
                    "data": {
                        "success": False,
                        "error": "u2_offline",
                        "action": action,
                    },
                    "channel": "u2",
                }
            return await self._execute_skill(script, action, params)

        # 微信 Hook：Frida 未连时尝试 lazy 重连（用户稍后手动打开微信的场景）
        if script == "wechat" and (not self.frida_mgr or not getattr(self.frida_mgr, "connected", False)):
            logger.info("[Frida] 未连接，尝试 lazy 重新初始化…")
            self._init_frida()

        # 通道1: Frida Hook（仅微信脚本有 wechat_hook RPC 映射）
        frida_result = None
        frida_err = ""
        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 = await self._run_frida_sync(
                    lambda: 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": "websocket/frida"}
                frida_err = str((frida_result or {}).get("error") or (frida_result or {}).get("message") or "RPC 返回 success=false")
                logger.debug(f"[Frida] {script}.{action} 执行失败: {frida_err}，降级 u2")
            except ImportError:
                frida_err = "hook_executor 不可用"
            except Exception as e:
                frida_err = str(e)
                logger.warning(f"[Frida] {script}.{action} 异常: {e}，降级 u2")

        if hook_only:
            # 真机铁律 #3/#14：诚实透传 Frida 原始失败原因（方法不存在 / RPC 异常 / success=false）
            return {
                "code": 503,
                "message": f"Hook 未成功（hook_only 禁止降级）: {frida_err or '微信 Frida 未附着'}",
                "data": {
                    "script": script,
                    "action": action,
                    "success": False,
                    "frida_connected": bool(
                        self.frida_mgr and self.frida_mgr.connected
                    ),
                    "frida_error": frida_err,
                    "frida_result": frida_result,
                },
            }

        # 通道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

                status = self._get_quick_status()
                status["u2"] = bool(self.d)

                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": status,
                }

                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 = self._connect_u2_with_fallback()
                self.d = d
                self._connection_guard = None
                logger.info(f"🔄 u2 重连成功({self.adb_serial}): {d.info.get('productName', 'Unknown')}")
                # G2：注册常早于 u2 就绪（model=Unknown），u2 连上后重发一次注册补真实硬件指纹
                if not getattr(self, "_hw_registered", False) and self.connected and self.ws:
                    self._hw_registered = True
                    try:
                        import asyncio as _a
                        _a.get_running_loop().create_task(self._register())
                        logger.info("🔄 u2 就绪，重发注册补硬件指纹 (G2)")
                    except Exception as _re:
                        logger.debug(f"G2 重注册调度失败: {_re}")
                return
            except Exception:
                if not self._running_on_device():
                    return

        if not self._running_on_device():
            return

        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
                if scan_count % 6 == 0:
                    result = await self._run_u2_sync(guard.full_guard_cycle)
                    popup_count = (result.get("popups") or {}).get("count", 0)
                    if not result.get("all_ok", True):
                        logger.warning(f"🛡️ 完整守护 #{scan_count}: 连接异常 {result}")
                    if popup_count > 0:
                        logger.info(f"🛡️ 完整守护 #{scan_count}: 处理了 {popup_count} 个弹窗")
                        if self.ws and self.connected:
                            await self._send_event("popup_dismissed", {
                                "dismissed": (result.get("popups") or {}).get("dismissed", []),
                                "scan_count": scan_count,
                            })
                else:
                    result = await self._run_u2_sync(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}
        # BIND-07：心跳附带寻服阶段，供中台实时显示连接来源（lan/primary/public/retry）
        status["connect_stage"] = getattr(self, "connect_stage", "primary")
        
        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:
                        level = int(battery_out.split(":")[1].strip())
                        status["battery"] = level
                        status["battery_level"] = level
                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":
                async with self._cmd_semaphore:
                    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":
                async with self._cmd_semaphore:
                    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 == "ai_chat":
                if self.ai_brain:
                    task_data = data.get("data", {})
                    instruction = (
                        task_data.get("instruction", "")
                        or task_data.get("message", "")
                        or task_data.get("task", "")
                    ).strip()
                    if not instruction:
                        await self._send_response(command_id, {"code": 400, "message": "instruction 不能为空"})
                    else:
                        result = await self.ai_brain.chat_and_execute(
                            instruction,
                            self._get_quick_status(),
                            self._execute_with_frida_priority,
                        )
                        await self._send_response(command_id, result)
                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 == "frida_reload":
                if self.frida_mgr:
                    result = await self._run_frida_sync(self.frida_mgr.reload_script)
                else:
                    self._init_frida()
                    if self.frida_mgr:
                        result = await self._run_frida_sync(self.frida_mgr.reload_script)
                    else:
                        result = {"success": False, "error": "Frida 未初始化"}
                await self._send_response(command_id, {
                    "code": 200 if result.get("success") else 503,
                    "message": "Hook 脚本已重载" if result.get("success") else "Hook 脚本重载失败",
                    "data": result,
                })

            elif msg_type == "command":
                action = data.get("action", "")
                params = data.get("params") or {}
                if action == "frida_connect":
                    if params.get("port"):
                        os.environ["WP_FRIDA_PORT"] = str(params.get("port"))
                    if params.get("mode"):
                        os.environ["WP_FRIDA_MODE"] = str(params.get("mode"))
                    self._init_frida()
                    connected = bool(self.frida_mgr and getattr(self.frida_mgr, "connected", False))
                    await self._send_response(command_id, {
                        "code": 200 if connected else 503,
                        "message": "Frida 已连接" if connected else "Frida 未初始化",
                        "data": {
                            "success": connected,
                            "wechat_attached": connected,
                            "supported_actions": 112 if connected else 0,
                        },
                    })
                elif action == "frida_disconnect":
                    if self.frida_mgr:
                        self.frida_mgr.stop()
                        self.frida_mgr = None
                    await self._send_response(command_id, {
                        "code": 200,
                        "message": "Frida 已断开",
                        "data": {"success": True},
                    })
                elif action == "frida_diagnostics":
                    diagnostics = {
                        "success": True,
                        "frida_connected": bool(self.frida_mgr and getattr(self.frida_mgr, "connected", False)),
                        "env_mode": os.environ.get("WP_FRIDA_MODE", ""),
                        "env_port": os.environ.get("WP_FRIDA_PORT", ""),
                    }
                    if self.d:
                        checks = {
                            "wechat_pid": "pidof com.tencent.mm || true",
                            "frida_pids": "pidof fs_3f823a frida-server || true",
                            "frida_listen": "cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep -i ':38C9\\|:69A2' || true",
                            "frida_binary": "ls -l /data/local/tmp/fs_3f823a /data/local/tmp/frida-server 2>/dev/null || true",
                        }
                        shell = {}
                        for key, cmd in checks.items():
                            try:
                                shell[key] = (self.d.shell(cmd, timeout=4).output or "").strip()
                            except Exception as e:
                                shell[key] = f"ERROR: {e}"
                        diagnostics["shell"] = shell
                    else:
                        diagnostics["success"] = False
                        diagnostics["error"] = "uiautomator2未连接"
                    await self._send_response(command_id, {
                        "code": 200 if diagnostics.get("success") else 503,
                        "message": "Frida 诊断完成" if diagnostics.get("success") else "Frida 诊断失败",
                        "data": diagnostics,
                    })
                else:
                    await self._send_response(command_id, {
                        "code": 400,
                        "message": f"未知 command action: {action}",
                        "data": {"success": False, "action": action},
                    })

            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未连接"}

            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 self._run_u2_sync(_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)
            
            # u2 分发别名：SDK 标准 action 名 ↔ 设备端 skill 方法名不一致时转换
            # （如二维码素材：generate_my_qr_code 的真实现是 skill.show_my_qr）
            _U2_ACTION_ALIASES = {
                "generate_my_qr_code": "show_my_qr",
                "generate_group_qr_code": "show_group_qr",
                "add_friend_by_qr": "scan_add_friend",
                "set_friend_remark": "set_remark",
                "revoke_message": "recall_message",
                "search_contacts": "search_contact",
                "get_contact_info": "get_friend_info",
                "set_group_announcement": "set_group_notice",
                "send_transfer": "transfer",
                "receive_transfer": "receive_transfer",
                "get_wallet_balance": "view_wallet",
                "get_transaction_history": "view_transactions",
                "show_payment_code": "show_payment_code",
                "receive_payment": "receive_payment",
                "batch_send": "mass_send",
                "get_safety_center": "safety_center",
                "get_top_stories": "top_stories",
                "get_wechat_steps": "get_steps",
                "like_wechat_steps": "like_steps",
                "global_search": "wechat_search",
                "get_recent_mini_programs": "get_recent_mini_programs",
                "share_mini_program": "share_mini_program",
                "send_voice": "send_voice_message",
                "send_file": "send_file_from_chat",
                "add_favorite": "add_to_favorites",
                "delete_favorite": "delete_favorite",
                "unfollow_official_account": "unfollow_official_account",
                "add_custom_emoji": "add_custom_emoji",
                "add_to_float": "add_to_float",
                "remove_from_float": "remove_from_float",
                "set_privacy": "set_moments_privacy",
                "set_do_not_disturb": "set_mute_chat",
                "pin_chat": "set_chat_top",
                "set_sex": "set_gender",
                "set_what_up": "set_status",
                "get_login_devices": "get_login_devices",
                "remove_login_device": "remove_login_device",
                "enable_fingerprint": "enable_fingerprint",
                "set_account_protection": "set_account_protection",
                "browse_channels": "get_video_list",
                "like_channel_video": "like_video",
                "comment_channel_video": "comment_video",
                "follow_channel": "follow_video_creator",
                "unfollow_channel": "unfollow_video_creator",
                "share_channel_video": "share_video",
                "forward_multiple": "forward_multiple",
                "get_official_account_articles": "get_official_account_articles",
                "reply_comment": "reply_comment",
            }
            resolved_action = action
            if not hasattr(skill, action) and action in _U2_ACTION_ALIASES:
                resolved_action = _U2_ACTION_ALIASES[action]
            # 调用方法
            method = getattr(skill, resolved_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:
                result = await self._run_u2_sync(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:
        """截图（raw 不支持时回退 PNG bytes）"""
        try:
            raw_bytes = None
            serial = self.adb_serial
            try:
                raw_bytes = subprocess.check_output(
                    ["adb", "-s", serial, "exec-out", "screencap", "-p"],
                    timeout=10,
                )
            except Exception:
                raw_bytes = None
            try:
                if raw_bytes is None:
                    raw_bytes = self.d.screenshot(format='raw')
            except Exception:
                shot = self.d.screenshot()
                if isinstance(shot, bytes):
                    raw_bytes = shot
                else:
                    import io
                    buf = io.BytesIO()
                    shot.save(buf, format='PNG')
                    raw_bytes = buf.getvalue()
            b64 = base64.b64encode(raw_bytes).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(raw_bytes),
                }
            }
        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),
                }

                # G2 防封修复（2026-05-31）：采集真实硬件指纹字段供 compute_fingerprint
                # 一机一指纹；getprop 失败不阻断注册。
                try:
                    _props = (
                        "ro.product.manufacturer ro.product.model ro.product.brand "
                        "ro.build.version.sdk ro.product.cpu.abi ro.serialno ro.build.fingerprint"
                    )
                    _out = self.d.shell(f"for p in {_props}; do echo \"$p=$(getprop $p)\"; done").output or ""
                    _kv = {}
                    for _line in _out.strip().splitlines():
                        if "=" in _line:
                            _k, _, _v = _line.partition("=")
                            _kv[_k.strip()] = _v.strip()
                    fp_map = {
                        "manufacturer": "ro.product.manufacturer",
                        "model": "ro.product.model",
                        "brand": "ro.product.brand",
                        "sdk_version": "ro.build.version.sdk",
                        "cpu_abi": "ro.product.cpu.abi",
                        "serial": "ro.serialno",
                        "fingerprint": "ro.build.fingerprint",
                    }
                    for _field, _prop in fp_map.items():
                        _val = _kv.get(_prop, "")
                        if _val and _val.lower() not in ("", "unknown"):
                            info[_field] = _val
                    # android_id（settings secure）
                    try:
                        _aid = (self.d.shell("settings get secure android_id").output or "").strip()
                        if _aid and _aid.lower() != "null":
                            info["android_id"] = _aid
                    except Exception:
                        pass
                    # 屏幕宽高与密度（供 compute_fingerprint 扁平键）
                    info["screen_width"] = d_info.get("displayWidth", 0)
                    info["screen_height"] = d_info.get("displayHeight", 0)
                    if d_info.get("displaySizeDpX"):
                        info["density"] = d_info.get("displaySizeDpX")
                except Exception as _fe:
                    logger.debug(f"[G2] 硬件指纹采集失败(不阻断): {_fe}")
                
                # 检测已安装的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
        # BIND-07：上报当前寻服阶段与候选数，供中台/工作台显示连接来源
        info["connect_stage"] = getattr(self, "connect_stage", "primary")
        info["server_url"] = self.server_url
        info["server_candidate_count"] = len(getattr(self, "server_candidates", []) or [])

        return info
    
    # ====================================================================
    # 六、启动/停止
    # ====================================================================
    
    async def start(self):
        """启动Agent"""
        self._loop = asyncio.get_running_loop()
        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")
    )
    auto_discover = (
        os.environ.get("WP_AUTO_DISCOVER", "1").lower() in ("1", "true", "yes")
        or getattr(args, "discover", False)
    )
    if not server_base and auto_discover:
        try:
            from sdk_discovery import discover_sdk_ws_base
            discovered = discover_sdk_ws_base(timeout=float(os.environ.get("WP_DISCOVER_TIMEOUT", "20")))
            if discovered:
                server_base = discovered
                logger.info(f"📡 UDP 发现 SDK: {server_base}")
        except Exception as e:
            logger.warning(f"SDK 自动发现失败: {e}")
    if not server_base:
        server_base = "ws://192.168.1.100:8899/ws/device"
    server_base = server_base.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}"

    # BIND-03 公网主服有序回退列表（环境变量 > 命令行 > config.json.public_servers）
    public_raw = (
        os.environ.get("WP_PUBLIC_SERVERS")
        or getattr(args, "public_servers", None)
        or ""
    )
    public_servers = [s.strip() for s in public_raw.split(",") if s.strip()]
    if not public_servers and isinstance(config.get("public_servers"), list):
        public_servers = [str(s).strip() for s in config["public_servers"] if str(s).strip()]
    if public_servers:
        logger.info(f"🌐 公网主服回退列表: {public_servers}")

    # AI Brain 配置（环境变量 > config.json.ai_brain）
    ai_cfg = config.get("ai_brain", {})
    _ai_enabled_env = os.environ.get("WP_AI_ENABLED")
    if _ai_enabled_env is None:
        _ai_enabled = bool(ai_cfg.get("enabled", False))
    else:
        _ai_enabled = _ai_enabled_env.strip().lower() in ("1", "true", "yes", "on")
    ai_config = {
        "enabled": _ai_enabled,
        "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,
        "public_servers": public_servers,
    }


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基础地址（省略则 UDP 自动发现）')
    parser.add_argument('--discover', action='store_true', help='强制 UDP beacon 发现 SDK（默认 WP_AUTO_DISCOVER=1）')
    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）')
    parser.add_argument('--public-servers', dest='public_servers', default=None,
                        help='BIND-03 公网主服有序回退列表（逗号分隔，断 LAN 后逐个探测）')

    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"),
        public_servers=cfg.get("public_servers"),
    )

    # 信号处理：优雅关闭
    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()
