"""
工作手机SDK v3.0 - 统一接口路由
存客宝重点对接的接口

功能模块：
1. 消息管理 - 发送/获取消息
2. 好友管理 - 添加/通过好友请求
3. 群聊管理 - 创建群/邀请入群/群发消息
4. 标签管理 - 添加/删除/查询标签
5. 朋友圈管理 - 发布/点赞/评论
6. 联系人管理 - 获取联系人列表
"""

from fastapi import APIRouter, HTTPException, Body
from typing import Optional, List
from pydantic import BaseModel, Field
from enum import Enum
import asyncio
import logging
import re
import time

from services.ws_hub import ws_hub
from services.device_manager import device_manager
from services.adb_device import adb_manager
from services import ai_agent as ai_agent_service
from services.rate_limiter import rate_limiter, DailyLimitExceeded
from services.content_guard import diversify_content, filter_sensitive, has_sensitive_words
from services.account_lifecycle import account_lifecycle
from config import settings
from services.device_id_util import device_id_md5

router = APIRouter()
logger = logging.getLogger(__name__)

DEFAULT_CONTACT_PULL_LIMIT = 10000
MAX_CONTACT_PULL_LIMIT = 20000
DEFAULT_MESSAGE_PULL_LIMIT = 3000
MAX_MESSAGE_PULL_LIMIT = 5000


# ========== 枚举定义 ==========

class Platform(str, Enum):
    """支持的平台"""
    WECHAT = "wechat"
    DOUYIN = "douyin"
    XHS = "xhs"
    XIANYU = "xianyu"
    SOUL = "soul"


class MessageType(str, Enum):
    """消息类型"""
    TEXT = "text"
    IMAGE = "image"
    VIDEO = "video"
    VOICE = "voice"
    FILE = "file"
    LINK = "link"
    MINI_PROGRAM = "mini_program"


class Channel(str, Enum):
    """执行通道"""
    OFFICIAL_API = "official_api"
    SDK_CONTROL = "sdk_control"
    AI_AGENT = "ai_agent"
    HOOK = "hook"


# ========== 消息相关模型 ==========

class SendMessageRequest(BaseModel):
    """发送消息请求"""
    device_id: str
    platform: Platform
    to_id: str
    content: str
    msg_type: MessageType = MessageType.TEXT
    media_url: Optional[str] = None
    at_list: Optional[List[str]] = None  # @列表（群聊时使用）
    timeout_seconds: Optional[int] = None  # 本次请求超时(秒)，不传则用 MESSAGE_SEND_TIMEOUT
    channel: Optional[str] = None  # 强制通道（hook/sdk_control/ai_agent/official_api）
    hook_config: Optional[dict] = None  # Hook配置（script_id/method/timeout）


class SendMessageResponse(BaseModel):
    """发送消息响应"""
    success: bool
    message_id: Optional[str] = None
    channel_used: Channel
    error: Optional[str] = None


class GetMessagesRequest(BaseModel):
    """获取消息请求"""
    device_id: str
    platform: Platform
    conversation_id: Optional[str] = None
    limit: int = DEFAULT_MESSAGE_PULL_LIMIT
    offset: int = 0
    since_time: Optional[int] = None


class Message(BaseModel):
    """消息"""
    message_id: str
    from_id: str
    to_id: str
    content: str
    msg_type: MessageType
    timestamp: int
    is_self: bool


# ========== 好友相关模型 ==========

class AddFriendRequest(BaseModel):
    """添加好友请求"""
    device_id: str
    platform: Platform
    user_id: str
    message: str = ""
    source: Optional[str] = None  # 来源说明


class AcceptFriendRequest(BaseModel):
    """通过好友请求"""
    device_id: str
    platform: Platform
    user_id: str


class SetRemarkRequest(BaseModel):
    """设置备注请求"""
    device_id: str
    platform: Platform
    user_id: str
    remark: str


class DeleteFriendRequest(BaseModel):
    """删除好友请求"""
    device_id: str
    platform: Platform
    user_id: str


# ========== 群聊相关模型 ==========

class CreateGroupRequest(BaseModel):
    """创建群聊请求"""
    device_id: str
    platform: Platform
    group_name: str
    member_ids: List[str]  # 群成员ID列表


class InviteToGroupRequest(BaseModel):
    """邀请入群请求"""
    device_id: str
    platform: Platform
    group_id: str  # 群ID或群名
    member_ids: List[str]  # 邀请的成员ID


class RemoveFromGroupRequest(BaseModel):
    """移出群聊请求"""
    device_id: str
    platform: Platform
    group_id: str
    member_ids: List[str]


class SetGroupNoticeRequest(BaseModel):
    """设置群公告请求"""
    device_id: str
    platform: Platform
    group_id: str
    notice: str


class SetGroupNameRequest(BaseModel):
    """设置群名请求"""
    device_id: str
    platform: Platform
    group_id: str
    group_name: str


class GroupMessageRequest(BaseModel):
    """群发消息请求"""
    device_id: str
    platform: Platform
    group_id: str
    content: str
    msg_type: MessageType = MessageType.TEXT
    media_url: Optional[str] = None
    at_all: bool = False  # 是否@所有人
    at_list: Optional[List[str]] = None


class SetGroupWelcomeRequest(BaseModel):
    """设置群欢迎语请求"""
    device_id: str
    platform: Platform
    group_id: str
    welcome_text: str
    welcome_image: Optional[str] = None


# ========== 标签相关模型 ==========

class AddTagRequest(BaseModel):
    """添加标签请求"""
    device_id: str
    platform: Platform
    user_id: str
    tags: List[str]


class RemoveTagRequest(BaseModel):
    """移除标签请求"""
    device_id: str
    platform: Platform
    user_id: str
    tags: List[str]


class CreateTagRequest(BaseModel):
    """创建标签请求"""
    device_id: str
    platform: Platform
    tag_name: str


class DeleteTagRequest(BaseModel):
    """删除标签请求"""
    device_id: str
    platform: Platform
    tag_name: str


class GetUsersByTagRequest(BaseModel):
    """根据标签获取用户请求"""
    device_id: str
    platform: Platform
    tag_name: str
    limit: int = 100


# ========== 朋友圈相关模型 ==========

class PostMomentsRequest(BaseModel):
    """发布朋友圈请求"""
    device_id: str
    platform: Platform
    content: str
    images: Optional[List[str]] = None  # 图片URL列表
    video_url: Optional[str] = None
    location: Optional[str] = None  # 位置
    visible_list: Optional[List[str]] = None  # 可见名单
    invisible_list: Optional[List[str]] = None  # 不可见名单


class LikeMomentsRequest(BaseModel):
    """点赞朋友圈请求"""
    device_id: str
    platform: Platform
    user_id: str  # 朋友圈所属用户
    post_index: int = 0  # 朋友圈索引（第几条）


class CommentMomentsRequest(BaseModel):
    """评论朋友圈请求"""
    device_id: str
    platform: Platform
    user_id: str
    post_index: int = 0
    comment: str
    reply_to: Optional[str] = None  # 回复某人


class GetMomentsRequest(BaseModel):
    """获取朋友圈请求"""
    device_id: str
    platform: Platform
    user_id: Optional[str] = None  # 不指定则获取自己的
    limit: int = 10


class ReplyCommentRequest(BaseModel):
    """回复评论请求（抖音/小红书等）"""
    device_id: str
    platform: Platform
    video_id: Optional[str] = None
    comment_id: str
    content: str


# ========== 批量操作模型 ==========

class BatchSendMessageRequest(BaseModel):
    """批量发送消息请求"""
    device_id: str
    platform: Platform
    to_ids: List[str]
    content: str
    msg_type: MessageType = MessageType.TEXT
    media_url: Optional[str] = None
    interval: float = 2.0  # 发送间隔(秒)


class BatchAddFriendRequest(BaseModel):
    """批量添加好友请求"""
    device_id: str
    platform: Platform
    user_ids: List[str]
    message: str = ""
    interval: float = 5.0  # 添加间隔(秒)


class MessageSyncSinceRequest(BaseModel):
    """增量同步消息请求"""
    device_id: str
    platform: Platform = Platform.WECHAT
    conversation_id: Optional[str] = None
    since_time: int = 0
    limit: int = DEFAULT_MESSAGE_PULL_LIMIT
    offset: int = 0


# ========== 通道路由器 ==========

class ChannelRouter:
    """通道路由器 - 选择最优执行通道"""
    
    # 官方API能力矩阵
    OFFICIAL_API_SUPPORT = {
        Platform.DOUYIN: ["send_message", "get_messages", "get_fans", "reply_comment"],
        Platform.XIANYU: ["send_message", "get_messages"],
    }
    
    @staticmethod
    def route(platform: Platform, action: str, device_online: bool) -> Channel:
        """选择执行通道"""
        # 1. 检查官方API
        if platform in ChannelRouter.OFFICIAL_API_SUPPORT:
            if action in ChannelRouter.OFFICIAL_API_SUPPORT[platform]:
                return Channel.OFFICIAL_API

        # 2. 微信后端强制 Frida Hook 主控（矩阵 v8.0.56 真机验收口径）
        if platform == Platform.WECHAT and getattr(settings, "WECHAT_BACKEND_ONLY", True):
            if device_online:
                return Channel.HOOK
        
        # 3. 设备在线用SDK控制
        if device_online:
            return Channel.SDK_CONTROL
        
        # 4. 兜底用AI Agent
        return Channel.AI_AGENT


# ========== 辅助函数 ==========

def _check_device_online(device_id: str, platform: str = "wechat", action: str = ""):
    """检查设备是否在线（经 device_transport 统一策略，无线主控 WS 优先）。"""
    from services.device_transport import device_transport
    return device_transport.check_device_online(device_id, platform, action)


def _get_device_mode(device_id: str, platform: str = "wechat", action: str = "") -> str:
    """获取设备传输模式（经 device_transport 封装，禁止业务层自行判 ADB）。"""
    from services.device_transport import device_transport
    return device_transport.resolve_mode(device_id, platform, action)


def _payload_of(result: dict) -> dict:
    """兼容 {data:{...}} 与直接 payload 返回。"""
    if not isinstance(result, dict):
        return {}
    payload = result.get("data") if isinstance(result.get("data"), dict) else result
    return payload if isinstance(payload, dict) else {}


def _list_from_payload(payload: dict, *keys: str) -> list:
    """从不同 Hook/Agent 返回格式里抽取列表。"""
    if not isinstance(payload, dict):
        return []
    for key in keys:
        value = payload.get(key)
        if isinstance(value, list):
            return value
        if isinstance(value, dict):
            for nested in ("items", "list", "data"):
                nested_value = value.get(nested)
                if isinstance(nested_value, list):
                    return nested_value
    data = payload.get("data")
    if isinstance(data, dict):
        return _list_from_payload(data, *keys)
    return []


def _message_timestamp(message: dict) -> int:
    """把常见消息时间字段归一为秒级 int，无法识别时返回 0。"""
    if not isinstance(message, dict):
        return 0
    for key in ("timestamp", "create_time", "createTime", "time", "msg_time", "msgTime"):
        raw = message.get(key)
        if raw is None:
            continue
        try:
            ts = int(float(raw))
            return ts // 1000 if ts > 10_000_000_000 else ts
        except (TypeError, ValueError):
            continue
    return 0


def _matches_contact(contact: dict, keyword: str) -> bool:
    if not keyword:
        return True
    text = " ".join(str(contact.get(k, "")) for k in (
        "nickname", "display_name", "remark", "alias", "wechat_id", "wxid", "user_id", "username"
    ))
    return keyword.lower() in text.lower()


def _bounded_limit(value, default: int, maximum: int) -> int:
    try:
        num = int(value)
    except (TypeError, ValueError):
        num = default
    return min(max(num, 1), maximum)


def _bounded_offset(value) -> int:
    try:
        num = int(value)
    except (TypeError, ValueError):
        num = 0
    return max(num, 0)


def _parse_messages_from_xml(xml: str, limit: int) -> list:
    """从 UI 树 XML 解析 text 节点作为消息列表（ADB 模式用）"""
    out = []
    if not xml:
        return out
    skip = {"搜索", "发送", "输入", "消息", "私信", "通讯录"}
    for i, m in enumerate(re.finditer(r'\btext="([^"]{1,200})"', xml)):
        if i >= limit:
            break
        text = m.group(1).strip()
        if text and text not in skip:
            out.append({
                "message_id": f"adb_ui_{i}",
                "content": text,
                "from_id": "",
                "to_id": "",
                "timestamp": 0,
                "is_self": False,
            })
    return out


def _parse_contacts_from_xml(xml: str, limit: int) -> list:
    """从 UI 树 XML 解析 text 节点作为联系人列表（ADB 模式用）"""
    out = []
    if not xml:
        return out
    skip = {"通讯录", "搜索", "添加朋友", "新的朋友", "群聊", "标签", "公众号", "微信"}
    for i, m in enumerate(re.finditer(r'\btext="([^"]{1,100})"', xml)):
        if i >= limit:
            break
        text = m.group(1).strip()
        if text and text not in skip:
            out.append({"user_id": f"adb_contact_{i}", "nickname": text, "remark": ""})
    return out


async def _anti_ban_guard(device_id: str, platform: str, action: str, content: Optional[str] = None) -> dict:
    """
    防封守卫：在执行任何操作前进行防封检查。
    返回 {"pass": True/False, "reason": str, "content": str(处理后的内容)}
    检查链：时段 → 生命周期 → L1全局 → L2设备 → L3动作 → 敏感词 → 内容差异化
    """
    import os
    if os.environ.get("SDK_MATRIX_VERIFY") == "1":
        return {"pass": True, "reason": "", "content": content}

    from services.anti_ban_alert import (
        alert_daily_limit, alert_outside_hours, alert_sensitive_content,
    )
    from services.rate_limiter import (
        OutsideOperationHours, LongPauseRequired, HighRiskComboBlocked,
        SilentThrottleCooldown, classify_risk,
    )

    def _blocked(reason: str) -> dict:
        # AB-06：附带风险分级 risk_action（block/needs_human/log）
        return {"pass": False, "reason": reason, "content": content,
                "risk_action": classify_risk(reason)}

    try:
        lifecycle_check = await account_lifecycle.check_allowed(device_id, platform, action)
        if not lifecycle_check["allowed"]:
            return _blocked(lifecycle_check["reason"])
    except Exception as e:
        logger.warning(f"[anti_ban] 生命周期检查异常(跳过): {e}")

    try:
        is_new = await account_lifecycle.is_new_account(device_id, platform)
        wait = await rate_limiter.check_and_wait(device_id, platform, action, is_new_account=is_new)
        if wait > 0:
            logger.info(f"[anti_ban] {device_id}/{platform}.{action} 等待了 {wait:.1f}s")
    except OutsideOperationHours as e:
        asyncio.ensure_future(alert_outside_hours(device_id, action))
        return _blocked(str(e))
    except DailyLimitExceeded as e:
        asyncio.ensure_future(alert_daily_limit(device_id, platform, action, -1, -1))
        return _blocked(str(e))
    except SilentThrottleCooldown as e:
        # AB-02：静默限流熔断期，写类动作 fast-fail（risk=needs_human）
        return _blocked(str(e))
    except (LongPauseRequired, HighRiskComboBlocked) as e:
        # AB-03：长会话中断 / 高危组合互斥，fast-fail 不长 sleep
        return _blocked(str(e))
    except Exception as e:
        logger.warning(f"[anti_ban] 限流检查异常(跳过): {e}")

    safe_content = content
    if content:
        sensitive = has_sensitive_words(content)
        if sensitive:
            logger.warning(f"[anti_ban] 检测到敏感词: {sensitive}")
            asyncio.ensure_future(alert_sensitive_content(device_id, platform, sensitive))
        safe_content = diversify_content(content, add_invisible=True, add_emoji=False)

    return {"pass": True, "reason": "", "content": safe_content}


# 必须走 u2 UI 自动化的微信动作 — 真源：services/device_transport.py


async def _execute_skill(
    device_id: str,
    platform: str,
    action: str,
    params: dict,
    timeout: int = 120,
    hook_only: bool = False,
) -> dict:
    """执行技能并返回结果（Frida Hook 优先 / WebSocket / ADB），返回值附带 _channel_used。

    通道优先级（与项目“主控 = Frida，辅助 = ADB”的设计一致）：
      1) hook_only=True 或本地能走 Frida → 本地 FridaManager + HookExecutor
      2) WebSocket Agent 在线 → 下发 execute
      3) ADB 在线 → ADB UI 自动化
      4) 其他 → 设备离线
    """
    from services.device_transport import device_transport

    mode = _get_device_mode(device_id, platform, action)
    ws_hook_only = device_transport.ws_hook_only(platform)
    hook_only = device_transport.should_force_hook_only(platform, action, hook_only)

    # 〇-A、WebSocket + Frida 主控（经 device_transport 统一封装，禁止绕过 WS 直连 ADB）
    if ws_hook_only and mode != "adb":
        if not device_transport.is_ws_online(device_id):
            return device_transport.offline_payload(device_id)
        mode = "websocket"

    # 〇-B、Frida Hook 本地 ADB attach（仅 WECHAT_WS_HOOK_ONLY=false 时）
    if platform == "wechat" and mode == "adb" and not ws_hook_only:
        try:
            adb_device = adb_manager.get_device(device_id)
            if adb_device and adb_device.is_online():
                serial = getattr(adb_device, "serial", device_id)
                hook_result = await asyncio.get_running_loop().run_in_executor(
                    None,
                    lambda: _try_local_frida_action(serial, action, params),
                )
                if hook_result and hook_result.get("success"):
                    hook_result["_channel_used"] = "frida/hook"
                    return hook_result
                if hook_only:
                    return {
                        "code": 503,
                        "success": False,
                        "message": (hook_result or {}).get("error") or "Frida 主控不可用",
                        "_channel_used": "frida/hook(failed)",
                    }
                logger.warning(f"[hook] 本地 Frida 失败，降级到 {mode}: {hook_result}")
            elif hook_only:
                return {
                    "code": 503,
                    "success": False,
                    "message": "微信后端模式要求 ADB 在线 + Frida 可用；当前未检测到可用 ADB 设备",
                    "_channel_used": "none",
                }
        except Exception as e:
            logger.warning(f"[hook] 本地 Frida 异常：{e}")
            if hook_only:
                return {"code": 503, "success": False, "message": str(e), "_channel_used": "frida/hook(failed)"}

    if mode == "websocket":
        return await device_transport.execute_via_ws(
            device_id, platform, action, params, timeout=timeout, hook_only=hook_only,
        )

    elif mode == "adb":
        if hook_only:
            return {
                "code": 503,
                "message": "hook_only 需 Frida Hook 主控可用（已尝试本地 Frida 仍失败）",
                "success": False,
                "_channel_used": "none",
            }
        adb_device = adb_manager.get_device(device_id)
        if not adb_device:
            return {"success": False, "error": "ADB设备不可用", "_channel_used": "none"}
        
        adb_result = await _execute_via_adb(adb_device, platform, action, params)
        adb_result["_channel_used"] = "adb/sdk_control"
        return adb_result
    
    else:
        return {"success": False, "error": "设备离线", "_channel_used": "offline"}


async def _execute_via_adb(device, platform: str, action: str, params: dict, timeout: int = 30) -> dict:
    """通过ADB执行技能操作 — 微信走专用引擎，其他平台走通用 UI 自动化。超时返回 error 而非无限挂起。"""
    try:
        if platform == "wechat":
            from services.wechat_adb_engine import WeChatADBEngine
            engine = WeChatADBEngine(device)
            loop = asyncio.get_running_loop()
            adb_timeout = 90 if action == "send_message" else timeout
            result = await asyncio.wait_for(
                loop.run_in_executor(None, lambda: engine.execute(action, params)),
                timeout=adb_timeout
            )
            return result

        app_packages = {
            "douyin": "com.ss.android.ugc.aweme",
            "xhs": "com.xingin.xhs",
            "xianyu": "com.taobao.idlefish",
            "soul": "cn.soulapp.android",
        }
        package = app_packages.get(platform)

        if action == "screenshot":
            return device.screenshot()

        if action == "send_message":
            import time as _time
            to_id = params.get("to_id", "")
            content = params.get("content", "")
            if package:
                device.start_app(package)
                _time.sleep(2)
            device.click_text("搜索")
            _time.sleep(0.5)
            device.input_text(to_id)
            _time.sleep(1)
            device.click_text(to_id)
            _time.sleep(1)
            device.input_text(content, clear=False)
            _time.sleep(0.3)
            device.click_text("发送")
            return {"success": True, "message_id": f"adb_{int(_time.time()*1000)}", "mode": "adb"}

        logger.info(f"ADB通用执行: {platform}.{action} params={params}")
        return {"success": True, "action": action, "params": params, "mode": "adb",
                "note": "通过ADB UI自动化执行"}
    except asyncio.TimeoutError:
        logger.error(f"ADB执行超时: {platform}.{action} timeout={timeout}s")
        return {"success": False, "error": f"操作超时（{timeout}s）", "mode": "adb"}
    except Exception as e:
        logger.error(f"ADB执行失败: {e}")
        return {"success": False, "error": str(e), "mode": "adb"}


# =============================================================================
# 〇、Hawk Hook 统一执行入口（丝滑控制）
# =============================================================================

class HookExecuteRequest(BaseModel):
    """Hawk Hook 格式 — 单端点执行任意操作，任何应用可直连"""
    device_id: str
    platform: Platform = Platform.WECHAT
    action: str = Field(..., description="操作名：send_message / get_contacts / get_profile / ... 共 107 个")
    params: dict = Field(default_factory=dict, description="action 所需参数（键值对）")
    hook_only: bool = Field(
        default=False,
        description="为 True 时仅走 Frida Hook，失败不降级 u2/ADB（纯 HWK 联调）",
    )


HOOK_ALL_ACTIONS = {
    "消息发送":   ["send_message", "send_group_message"],
    "消息获取":   ["get_messages", "get_recent_messages", "search_messages"],
    "联系人":     ["get_contacts", "get_contact_info", "search_contacts"],
    "好友管理":   ["add_friend", "accept_friend", "delete_friend", "set_friend_remark", "get_friend_requests"],
    "群管理":     ["get_groups", "get_group_info", "get_group_members", "create_group", "invite_to_group", "remove_from_group", "set_group_announcement", "set_group_name", "quit_group"],
    "朋友圈":     ["post_moments", "get_moments", "like_moments", "comment_moments", "delete_moments"],
    "账号管理":   ["get_profile", "check_account_status", "set_nickname", "set_signature", "set_avatar", "set_sex", "set_region", "set_whats_up"],
    "账号安全":   ["unblock_self", "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", "get_wallet_balance", "get_transaction_history"],
    "二维码":     ["scan_qr_code", "generate_my_qr_code", "generate_group_qr_code", "add_friend_by_qr"],
    "视频号":     ["browse_channels", "like_channel_video", "comment_channel_video", "follow_channel", "unfollow_channel", "share_channel_video"],
    "标签":       ["get_labels", "create_label", "delete_label", "set_contact_label", "get_contacts_by_label"],
    "收藏":       ["get_favorites", "add_favorite", "delete_favorite"],
    "设置":       ["set_privacy", "set_notification", "clear_chat_history", "set_chat_background", "set_do_not_disturb", "pin_chat"],
    "搜索":       ["global_search"],
    "小程序":     ["open_mini_program", "get_recent_mini_programs", "share_mini_program"],
    "文件传输":   ["send_image", "send_video", "send_file", "send_voice", "send_location", "send_card", "send_link"],
    "消息转发":   ["forward_message", "forward_multiple", "revoke_message"],
    "注册/登录":  ["register_account", "login_by_password", "login_by_sms", "logout", "switch_account", "auto_register", "check_login_state", "get_sim_phone"],
    "公众号":     ["get_official_accounts", "follow_official_account", "unfollow_official_account", "get_official_account_articles"],
    "表情":       ["send_emoji", "add_custom_emoji"],
    "浮窗":       ["add_to_float", "remove_from_float"],
    "设备信息":   ["get_device_info", "get_storage_info", "get_network_info"],
    "系统":       ["get_hook_status", "get_process_info", "get_wechat_version", "batch_execute"],
}


@router.get("/antiban/status", response_model=dict, tags=["防封风控"])
async def antiban_status(device_id: Optional[str] = None):
    """AB-05 防封看板数据源：聚合限流配置 + 操作时段 + 设备 guard 红灯 / RiskSentinel。

    - 无 device_id：返回全局限流策略与当前操作时段。
    - 带 device_id：附加该设备 register/心跳上报的 anti_ban（device_guard.redlight、RiskSentinel）。
    """
    from services.rate_limiter import PLATFORM_LIMITS, OPERATION_HOURS

    data = {
        "operation_hours": {
            "range": list(OPERATION_HOURS),
            "in_window": rate_limiter.check_operation_hours(),
            "tz": "Asia/Shanghai",
        },
        "platform_limits": PLATFORM_LIMITS,
        "long_pause": {"every_actions": "8-12", "pause_seconds": "600-1800"},
        "high_risk_exclusive": [
            "add_friend", "batch_add_friend", "batch_send",
            "mass_send", "create_group", "invite_to_group",
        ],
    }
    if device_id:
        info = ws_hub.get_device_info(device_id) or {}
        anti = info.get("anti_ban") or {}
        guard = anti.get("guard") or {}
        data["device"] = {
            "device_id": device_id,
            "online": ws_hub.is_online(device_id),
            "redlight": guard.get("redlight"),
            "guard_warnings": guard.get("warnings"),
            "sentinel": anti.get("sentinel"),
            "nurture": anti.get("nurture"),
            # AB-02/03 运行态：熔断 / 长暂停 / 动作节拍 / 最近高危动作
            "runtime": rate_limiter.get_runtime_state(device_id),
        }
    return {"code": 200, "data": data}


@router.get("/hook/actions", response_model=dict, tags=["Hook模块管理"])
async def hook_actions():
    """获取 Hook 支持的全部操作清单 — 107 个操作 / 24 个模块"""
    total = sum(len(v) for v in HOOK_ALL_ACTIONS.values())
    return {
        "code": 200,
        "total_actions": total,
        "total_modules": len(HOOK_ALL_ACTIONS),
        "modules": HOOK_ALL_ACTIONS,
    }


@router.get("/hook/probe/{device_id}", response_model=dict, tags=["Hook模块管理"])
async def hook_probe(device_id: str):
    """
    探测设备 Frida Hook 能力 — 连接 → ping → 版本 → 更新 device_modules

    无线主控（WORKPHONE_WS_FIRST）：Agent WebSocket 在线时经 WS 探测，不依赖主机 ADB。
    """
    import asyncio

    ws_first = getattr(settings, "WORKPHONE_WS_FIRST", True)
    ws_hook_only = getattr(settings, "WECHAT_WS_HOOK_ONLY", True)
    host_adb_probe = getattr(settings, "WORKPHONE_HOST_ADB_PROBE", False)

    if (ws_first or ws_hook_only) and ws_hub.is_online(device_id):
        result = await _probe_device_via_ws(device_id)
    elif host_adb_probe:
        adb_dev = adb_manager.get_device(device_id)
        serial = adb_dev.serial if adb_dev else device_id
        result = await asyncio.get_running_loop().run_in_executor(
            None, lambda: _probe_device_frida(serial),
        )
    else:
        result = {
            "supports_hook": False,
            "frida_version": "",
            "root_status": False,
            "wechat_version": "",
            "profile": {},
            "hook_tests": {"connect": "websocket-only: agent offline or frida not ready"},
            "transport": "websocket",
            "probe_detail": "无线主控：需设备端 Termux Agent + 本机 Frida，无需主机 ADB",
        }

    from services.hook_module_service import hook_module_service
    await hook_module_service.update_device_probe(device_id, {
        "supports_hook": result.get("supports_hook", False),
        "frida_version": result.get("frida_version", ""),
        "root_status": result.get("root_status", False),
    })

    return {"code": 200, "device_id": device_id, "device_id_md5": device_id_md5(device_id), **result}


async def _probe_device_via_ws(device_id: str) -> dict:
    """经 WebSocket Agent 探测 Frida（无线主控，无需主机 ADB）。"""
    info = ws_hub.get_device_info(device_id) or {}
    capabilities = set(info.get("capabilities") or [])
    reported_ready = bool(info.get("frida_available")) or bool(
        {"frida", "hook", "frida_rpc"} & capabilities
    )
    probe = {
        "supports_hook": reported_ready,
        "frida_version": "",
        "root_status": bool(info.get("root_status")),
        "wechat_version": "",
        "profile": {},
        "hook_tests": {},
        "transport": "websocket",
        "frida_available_reported": reported_ready,
    }

    ping = await _execute_skill(device_id, "wechat", "ping", {}, timeout=45, hook_only=True)
    probe["hook_tests"]["ping"] = str(ping)[:300]
    pdata = ping.get("data") if isinstance(ping.get("data"), dict) else {}
    frida_ok = (
        reported_ready
        or bool(pdata.get("frida_connected"))
        or bool(ping.get("success"))
        or "pong" in str(ping).lower()
    )
    probe["supports_hook"] = frida_ok
    probe["hook_tests"]["connect"] = "ok" if frida_ok else "failed"

    if frida_ok:
        # 性能优化：version 与 profile 为独立只读调用，并行执行（墙钟取最大值而非求和），
        # 显著缩短 probe 总耗时（原串行 ping+ver+profile ~38s）。
        ver, prof = await asyncio.gather(
            _execute_skill(device_id, "wechat", "get_wechat_version", {}, timeout=30, hook_only=True),
            _execute_skill(device_id, "wechat", "get_profile", {}, timeout=45, hook_only=True),
        )
        vdata = ver.get("data") if isinstance(ver.get("data"), dict) else {}
        probe["wechat_version"] = str(vdata.get("version") or vdata.get("wechat_version") or ver.get("wechat_version") or "")
        prdata = prof.get("data") if isinstance(prof.get("data"), dict) else {}
        if isinstance(prdata.get("data"), dict):
            probe["profile"] = prdata["data"]
        elif prdata:
            probe["profile"] = prdata

    if not probe["supports_hook"]:
        probe["probe_detail"] = (
            "Agent WS 在线但 Frida 未 attach；请在手机 Termux 运行 Agent（127.0.0.1 frida-server），无需 Mac ADB"
        )
    return probe


@router.get("/stability/watch", response_model=dict, tags=["设备稳定性"])
async def stability_watch(device_id: str, samples: int = 1, interval_seconds: float = 0):
    """
    设备稳定性采样入口：WS/Hook/ADB/耗时统一返回，供 24h 长稳脚本循环调用。
    """
    samples = min(max(int(samples or 1), 1), 120)
    interval_seconds = min(max(float(interval_seconds or 0), 0), 60)
    rows = []
    for index in range(samples):
        started = time.time()
        online = ws_hub.is_online(device_id)
        info = ws_hub.get_device_info(device_id) or {}
        adb_dev = adb_manager.get_device(device_id)
        try:
            probe = await _probe_device_via_ws(device_id) if online else {
                "supports_hook": False,
                "transport": "websocket",
                "hook_tests": {"connect": "agent_offline"},
            }
            hook_ok = bool(probe.get("supports_hook") or probe.get("hook_tests", {}).get("connect") == "ok")
            error = ""
        except Exception as exc:
            probe = {}
            hook_ok = False
            error = str(exc)[:200]
        elapsed_ms = int((time.time() - started) * 1000)
        rows.append({
            "index": index + 1,
            "ts": int(time.time()),
            "device_id": device_id,
            "device_id_md5": device_id_md5(device_id),
            "ws_online": online,
            "adb_online": bool(adb_dev),
            "adb_serial": getattr(adb_dev, "serial", ""),
            "hook_ok": hook_ok,
            "wechat_version": probe.get("wechat_version", ""),
            "transport": probe.get("transport") or info.get("transport") or "websocket",
            "latency_ms": elapsed_ms,
            "error": error,
        })
        if index < samples - 1 and interval_seconds:
            await asyncio.sleep(interval_seconds)

    ok_count = sum(1 for row in rows if row["ws_online"] and row["hook_ok"])
    return {
        "code": 200,
        "data": {
            "device_id": device_id,
            "device_id_md5": device_id_md5(device_id),
            "samples": rows,
            "summary": {
                "total": len(rows),
                "ok": ok_count,
                "success_rate": round(ok_count / len(rows), 4) if rows else 0,
                "max_latency_ms": max((row["latency_ms"] for row in rows), default=0),
            },
        },
    }


@router.get("/hook/data/{device_id}", response_model=dict, tags=["Hook模块管理"])
async def hook_data(
    device_id: str,
    modules: str = "profile,contacts,groups,messages,labels",
    contact_limit: int = DEFAULT_CONTACT_PULL_LIMIT,
    message_limit: int = DEFAULT_MESSAGE_PULL_LIMIT,
    contact_offset: int = 0,
    message_offset: int = 0,
):
    """
    一次性获取设备的 Hook 数据（联系人/消息/群/标签/资料等）

    modules 参数用逗号分隔，可选值：
    profile, contacts, groups, messages, labels, moments, accounts, hook_status, device_info
    """
    import asyncio

    requested = [m.strip() for m in modules.split(",") if m.strip()]
    limits = {
        "contact_limit": _bounded_limit(contact_limit, DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT),
        "message_limit": _bounded_limit(message_limit, DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT),
        "contact_offset": _bounded_offset(contact_offset),
        "message_offset": _bounded_offset(message_offset),
    }

    if ws_hub.is_online(device_id):
        result = await _fetch_hook_data_via_ws(device_id, requested, limits)
        return {"code": 200, "device_id": device_id, "device_id_md5": device_id_md5(device_id), **result}

    adb_dev = adb_manager.get_device(device_id)
    serial = adb_dev.serial if adb_dev else device_id
    result = await asyncio.get_running_loop().run_in_executor(
        None, lambda: _fetch_hook_data(serial, requested, limits),
    )
    return {"code": 200, "device_id": device_id, "device_id_md5": device_id_md5(device_id), **result}


@router.post("/hook/execute", response_model=dict, tags=["Hook模块管理"])
async def hook_execute(req: HookExecuteRequest):
    """
    Hawk Hook 统一执行 — 单接口控制整台手机

    任意应用只需调此端点，传 action + params 即可执行 107 种操作。
    通道优先级：Frida Hook → WebSocket Agent → ADB UI自动化。

    示例:
    ```json
    {"device_id":"dc9c23e00510","platform":"wechat","action":"send_message","params":{"to_id":"阿猫","content":"你好"}}
    {"device_id":"dc9c23e00510","platform":"wechat","action":"get_profile","params":{},"hook_only":true}
    {"device_id":"dc9c23e00510","platform":"wechat","action":"register_account","params":{"phone":"13800138000","nickname":"卡若AI"}}
    ```
    `hook_only:true` 时仅 Frida RPC，失败不降级 u2（需 Agent 已连上且 Frida 附着微信）。
    """
    _check_device_online(req.device_id)
    # 易阻塞动作快速失败（如 check_login_state 的 WCDB 加密读会挂 ~58s），
    # 缩短超时让其尽快落 u2 兜底而非拖垮通道。
    _SLOW_ACTION_TIMEOUTS = {"check_login_state": 20}
    _action_timeout = _SLOW_ACTION_TIMEOUTS.get(req.action, 120)
    result = await _execute_skill(
        req.device_id,
        req.platform.value,
        req.action,
        req.params or {},
        timeout=_action_timeout,
        hook_only=req.hook_only,
    )
    payload = result.get("data") if result.get("data") is not None else result
    channel = result.get("_channel_used") or result.get("channel") or result.get("mode") or "sdk_control"
    # 真机铁律 #3/#14：诚实传播内层状态，禁止把失败/未验证(503/success:False)伪装成 200
    inner_code = result.get("code")
    inner_success = result.get("success")
    if isinstance(payload, dict) and "success" in payload:
        inner_success = payload.get("success")
    code = inner_code if isinstance(inner_code, int) else 200
    if code == 200 and inner_success is False:
        code = 503  # hook 未真实成功（如 no_receiver_registered / Frida 主控不可用）
    return {"code": code, "data": payload, "action": req.action, "channel_used": channel}


# =============================================================================
# 一、消息管理接口
# =============================================================================

@router.post("/message/send", response_model=dict, tags=["消息管理"])
async def send_message(req: SendMessageRequest):
    """
    发送消息（统一接口）
    
    自动选择最优通道：
    1. 有官方API → 用API
    2. 设备在线 → 用SDK控制
    3. 兜底 → 用AI Agent
    
    超时：使用 MESSAGE_SEND_TIMEOUT（默认60s），超时返回 200 + success=false + error=timeout
    """
    # ---- 防封守卫 ----
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "send_message", req.content)
    if not guard["pass"]:
        return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}, "channel_used": "none"}
    req.content = guard["content"]

    # to_id 显示名归一（文件传输助手 → filehelper）
    _to = (req.to_id or "").strip()
    if _to in ("文件传输助手", "File Transfer"):
        req.to_id = "filehelper"

    mode = _get_device_mode(req.device_id)
    device_online = mode != "offline"
    forced_channel = (req.channel or "").strip().lower()
    if forced_channel:
        try:
            channel = Channel(forced_channel)
        except ValueError:
            raise HTTPException(status_code=400, detail=f"无效 channel: {req.channel}")
    else:
        channel = ChannelRouter.route(req.platform, "send_message", device_online)
    logger.info(f"[message/send] device_id={req.device_id} platform={req.platform.value} to_id={req.to_id} channel={channel.value}")

    try:
        if channel == Channel.OFFICIAL_API:
            result = await _send_via_official_api(req)
        elif channel == Channel.HOOK:
            result = await _send_via_hook(req)
        elif channel == Channel.SDK_CONTROL:
            result = await _send_via_sdk(req)
        else:
            result = await _send_via_agent(req)

        logger.info(f"[message/send] result success={result.get('success')} error={result.get('error')}")
        await device_manager.log_command(
            req.device_id,
            f"{req.platform.value}.send_message",
            req.model_dump(),
            result
        )
        err = result.get("error") or ""
        data = {
            "success": result.get("success", False),
            "message_id": result.get("message_id"),
            "error": err or None
        }
        if err and "未找到" in str(err):
            data["error_code"] = "contact_not_found"
        elif err and str(err) == "timeout":
            data["error_code"] = "timeout"
        # AB-02 静默限流识别：success=True 但无 message_id/svr_id 视为可能的静默降权，
        # 触发设备级熔断（写类动作冷却 30-60min，逐次退避升级；不伪造成功）。
        mid = result.get("message_id") or result.get("svr_id") or result.get("msg_id")
        if data["success"] and not mid:
            cooldown = rate_limiter.trip_silent_throttle(req.device_id)
            data["risk"] = "silent_throttle"
            data["risk_detail"] = f"发送返回成功但无 message_id/svr_id，疑似静默限流；已熔断写操作 {cooldown:.0f}s"
            logger.warning(f"[message/send] silent_throttle device_id={req.device_id} to_id={req.to_id} channel={channel.value} cooldown={cooldown:.0f}s")
        elif data["success"] and mid:
            rate_limiter.reset_silent_throttle(req.device_id)
        ch_used = result.get("channel_used") or channel.value
        return {"code": 200, "data": data, "channel_used": ch_used}
    except Exception as e:
        logger.warning(f"[message/send] exception channel={channel.value} e={e}")
        if channel == Channel.SDK_CONTROL:
            try:
                result = await _send_via_agent(req)
                return {
                    "code": 200,
                    "data": result,
                    "channel_used": Channel.AI_AGENT.value
                }
            except Exception:
                pass
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/message/list", response_model=dict, tags=["消息管理"])
async def get_messages(req: GetMessagesRequest):
    """获取消息列表"""
    
    _check_device_online(req.device_id)
    limit = _bounded_limit(req.limit, DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
    offset = _bounded_offset(req.offset)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "get_messages",
        {"conversation_id": req.conversation_id, "limit": limit, "offset": offset}
    )
    # 兼容 WebSocket 返回 { code, data: { messages } } 与 ADB 返回 { success, messages }
    payload = result.get("data") if result.get("data") is not None else result
    messages = payload.get("messages", [])
    return {
        "code": 200,
        "data": {
            "messages": messages,
            "count": len(messages),
            "requested_limit": limit,
            "offset": offset,
            "total_count": payload.get("total_count") or payload.get("total"),
            "has_more": bool(payload.get("has_more")) or len(messages) >= limit,
        },
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/message/sync-since", response_model=dict, tags=["消息管理"])
async def sync_messages_since(req: MessageSyncSinceRequest):
    """按时间增量同步消息，供存客宝/超管轮询拉取。"""
    _check_device_online(req.device_id)
    limit = _bounded_limit(req.limit, DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
    offset = _bounded_offset(req.offset)
    result = await _execute_skill(
        req.device_id,
        req.platform.value,
        "get_messages",
        {"conversation_id": req.conversation_id, "limit": limit, "offset": offset}
    )
    payload = _payload_of(result)
    messages = _list_from_payload(payload, "messages", "items", "list")
    filtered = []
    max_ts = int(req.since_time or 0)
    for message in messages:
        ts = _message_timestamp(message)
        if ts >= int(req.since_time or 0):
            filtered.append(message)
        if ts > max_ts:
            max_ts = ts
    return {
        "code": 200,
        "data": {
            "messages": filtered,
            "count": len(filtered),
            "since_time": int(req.since_time or 0),
            "next_since_time": max_ts,
            "requested_limit": limit,
            "offset": offset,
            "total_count": payload.get("total_count") or payload.get("total"),
            "has_more": len(messages) >= limit and len(filtered) >= limit,
        },
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/message/batch-send", response_model=dict, tags=["消息管理"])
async def batch_send_message(req: BatchSendMessageRequest):
    """
    批量发送消息（服务端逐条下发，间隔防风控，单条超时可控）
    """
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "batch_send", req.content)
    if not guard["pass"]:
        return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}, "channel_used": "none"}
    req.content = guard["content"]

    _check_device_online(req.device_id)
    per_msg_timeout = min(60, max(15, getattr(settings, "MESSAGE_SEND_TIMEOUT", 60)))
    last_channel = "none"
    sent, failed = [], []
    for i, to_id in enumerate(req.to_ids):
        if i > 0:
            await asyncio.sleep(max(0.5, min(float(req.interval), 30)))
        normalized_to_id = (to_id or "").strip()
        if normalized_to_id in ("文件传输助手", "File Transfer"):
            normalized_to_id = "filehelper"
        send_req = SendMessageRequest(
            device_id=req.device_id,
            platform=req.platform,
            to_id=normalized_to_id,
            content=req.content,
            msg_type=req.msg_type,
            media_url=req.media_url,
            timeout_seconds=per_msg_timeout,
        )
        result = await _send_via_hook(send_req)
        last_channel = result.get("channel_used") or last_channel
        if result.get("success"):
            sent.append({"to_id": normalized_to_id, "message_id": result.get("message_id")})
        else:
            failed.append({"to_id": normalized_to_id, "error": result.get("error") or "unknown"})
    return {
        "code": 200,
        "data": {"sent": sent, "failed": failed, "total": len(req.to_ids)},
        "channel_used": last_channel
    }


@router.post("/comment/reply", response_model=dict, tags=["消息管理"])
async def reply_comment(req: ReplyCommentRequest):
    """回复评论（抖音/小红书等）"""
    _check_device_online(req.device_id)
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "comment", req.content)
    if not guard["pass"]:
        return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}
    result = await _execute_skill(
        req.device_id, req.platform.value, "reply_comment",
        {"video_id": req.video_id, "comment_id": req.comment_id, "content": guard.get("content", req.content)}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {
        "code": 200,
        "data": payload,
        "channel_used": result.get("_channel_used", "sdk_control")
    }


# =============================================================================
# 二、好友管理接口
# =============================================================================

@router.post("/friend/add", response_model=dict, tags=["好友管理"])
async def add_friend(req: AddFriendRequest):
    """添加好友"""
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "add_friend", req.message)
    if not guard["pass"]:
        return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}, "channel_used": "none"}
    req.message = guard["content"] or req.message

    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "add_friend",
        {"user_id": req.user_id, "message": req.message, "source": req.source}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/friend/accept", response_model=dict, tags=["好友管理"])
async def accept_friend(req: AcceptFriendRequest):
    """通过好友请求"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "accept_friend",
        {"user_id": req.user_id}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/friend/set-remark", response_model=dict, tags=["好友管理"])
async def set_friend_remark(req: SetRemarkRequest):
    """设置好友备注"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_remark",
        {"user_id": req.user_id, "remark": req.remark}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/friend/delete", response_model=dict, tags=["好友管理"])
async def delete_friend(req: DeleteFriendRequest):
    """删除好友"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "delete_friend",
        {"user_id": req.user_id}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/friend/batch-add", response_model=dict, tags=["好友管理"])
async def batch_add_friend(req: BatchAddFriendRequest):
    """批量添加好友"""
    _check_device_online(req.device_id)
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "add_friend", req.message)
    if not guard["pass"]:
        return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}
    result = await _execute_skill(
        req.device_id, req.platform.value, "batch_add_friend",
        {
            "user_ids": req.user_ids,
            "message": guard.get("content", req.message),
            "interval": req.interval
        },
        timeout=len(req.user_ids) * 15
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.get("/contacts", response_model=dict, tags=["好友管理"])
async def get_contacts(device_id: str, platform: Platform, limit: int = DEFAULT_CONTACT_PULL_LIMIT, offset: int = 0):
    """获取联系人列表（存客宝字段完整：display_name / wechat_id / tags 等）"""
    from services.wechat_contact_normalizer import normalize_wechat_contacts

    _check_device_online(device_id)
    limit = _bounded_limit(limit, DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT)
    offset = _bounded_offset(offset)
    result = await _execute_skill(
        device_id, platform.value, "get_contacts",
        {"limit": limit, "offset": offset}
    )
    payload = result.get("data") if result.get("data") is not None else result
    raw = payload.get("contacts", [])
    contacts = normalize_wechat_contacts(raw) if platform == Platform.WECHAT else raw
    return {
        "code": 200,
        "data": {
            "contacts": contacts,
            "count": len(contacts),
            "requested_limit": limit,
            "offset": offset,
            "total_count": payload.get("total_count") or payload.get("total") or (payload.get("_diag") or {}).get("filtered_total"),
            "raw_total_count": (payload.get("_diag") or {}).get("rcontact_total"),
            "has_more": bool(payload.get("has_more")) or len(contacts) >= limit,
        },
        "channel_used": result.get("_channel_used", "sdk_control")
    }


# =============================================================================
# 三、群聊管理接口
# =============================================================================

@router.post("/group/create", response_model=dict, tags=["群聊管理"])
async def create_group(req: CreateGroupRequest):
    """创建群聊"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "create_group",
        {"group_name": req.group_name, "member_ids": req.member_ids}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/group/invite", response_model=dict, tags=["群聊管理"])
async def invite_to_group(req: InviteToGroupRequest):
    """邀请入群"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "invite_to_group",
        {"group_id": req.group_id, "member_ids": req.member_ids}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/group/remove", response_model=dict, tags=["群聊管理"])
async def remove_from_group(req: RemoveFromGroupRequest):
    """移出群聊"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "remove_from_group",
        {"group_id": req.group_id, "member_ids": req.member_ids}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/group/set-notice", response_model=dict, tags=["群聊管理"])
async def set_group_notice(req: SetGroupNoticeRequest):
    """设置群公告"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_group_notice",
        {"group_id": req.group_id, "notice": req.notice}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/group/set-name", response_model=dict, tags=["群聊管理"])
async def set_group_name(req: SetGroupNameRequest):
    """设置群名"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_group_name",
        {"group_id": req.group_id, "group_name": req.group_name}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/group/send-message", response_model=dict, tags=["群聊管理"])
async def send_group_message(req: GroupMessageRequest):
    """发送群消息"""
    _check_device_online(req.device_id)
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "send_group_message", req.content)
    if not guard["pass"]:
        return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}
    result = await _execute_skill(
        req.device_id, req.platform.value, "send_group_message",
        {
            "group_id": req.group_id,
            "content": guard.get("content", req.content),
            "msg_type": req.msg_type.value,
            "media_url": req.media_url,
            "at_all": req.at_all,
            "at_list": req.at_list
        }
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/group/set-welcome", response_model=dict, tags=["群聊管理"])
async def set_group_welcome(req: SetGroupWelcomeRequest):
    """设置群欢迎语"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_group_welcome",
        {
            "group_id": req.group_id,
            "welcome_text": req.welcome_text,
            "welcome_image": req.welcome_image
        }
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.get("/group/list", response_model=dict, tags=["群聊管理"])
async def get_group_list(device_id: str, platform: Platform = Platform.WECHAT, limit: int = 100, offset: int = 0):
    """获取群聊列表"""
    
    _check_device_online(device_id)
    limit = _bounded_limit(limit, 100, 500)
    offset = _bounded_offset(offset)
    
    result = await _execute_skill(
        device_id, platform.value, "get_groups",
        {"limit": limit, "offset": offset}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.get("/group/members", response_model=dict, tags=["群聊管理"])
async def get_group_members(device_id: str, group_id: str, platform: Platform = Platform.WECHAT):
    """获取群成员列表"""
    
    _check_device_online(device_id)
    
    result = await _execute_skill(
        device_id, platform.value, "get_group_members",
        {"group_id": group_id}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


# =============================================================================
# 四、标签管理接口
# =============================================================================

@router.post("/tag/add", response_model=dict, tags=["标签管理"])
async def add_tag(req: AddTagRequest):
    """给好友添加标签"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "add_tag",
        {"user_id": req.user_id, "tags": req.tags}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/tag/remove", response_model=dict, tags=["标签管理"])
async def remove_tag(req: RemoveTagRequest):
    """移除好友标签"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "remove_tag",
        {"user_id": req.user_id, "tags": req.tags}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/tag/create", response_model=dict, tags=["标签管理"])
async def create_tag(req: CreateTagRequest):
    """创建标签"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "create_tag",
        {"tag_name": req.tag_name}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/tag/delete", response_model=dict, tags=["标签管理"])
async def delete_tag(req: DeleteTagRequest):
    """删除标签"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "delete_tag",
        {"tag_name": req.tag_name}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.get("/tag/list", response_model=dict, tags=["标签管理"])
async def get_tag_list(device_id: str, platform: Platform, limit: int = 100, offset: int = 0):
    """获取标签列表"""
    
    _check_device_online(device_id)
    limit = _bounded_limit(limit, 100, 500)
    offset = _bounded_offset(offset)
    
    result = await _execute_skill(
        device_id, platform.value, "get_tags", {"limit": limit, "offset": offset}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/tag/users", response_model=dict, tags=["标签管理"])
async def get_users_by_tag(req: GetUsersByTagRequest):
    """根据标签获取好友列表"""
    
    _check_device_online(req.device_id)
    
    result = await _execute_skill(
        req.device_id, req.platform.value, "get_users_by_tag",
        {"tag_name": req.tag_name, "limit": req.limit}
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


# =============================================================================
# 五、朋友圈管理接口
# =============================================================================

@router.post("/moments/post", response_model=dict, tags=["朋友圈管理"])
async def post_moments(req: PostMomentsRequest):
    """发布朋友圈/瞬间（统一返回 200，业务失败用 success=false 表示，避免 502）"""
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "post_moments", req.content)
    if not guard["pass"]:
        return {"code": 200, "success": False, "error": guard["reason"], "risk_action": guard.get("risk_action"), "data": {}, "channel_used": "none"}
    req.content = guard["content"]

    try:
        _check_device_online(req.device_id)
    except HTTPException:
        raise
    try:
        result = await _execute_skill(
            req.device_id, req.platform.value, "post_moments",
            {
                "content": req.content,
                "images": req.images,
                "video_url": req.video_url,
                "location": req.location,
                "visible_list": req.visible_list,
                "invisible_list": req.invisible_list
            },
            timeout=60  # 发布朋友圈可能需要较长时间
        )
        rc = result.get("code", 200)
        if rc != 200:
            return {
                "code": 200,
                "success": False,
                "error": result.get("message", result.get("error", "发布失败")),
                "data": result.get("data", {}),
                "channel_used": result.get("_channel_used", "sdk_control")
            }
        return {
            "code": 200,
            "success": True,
            "data": result.get("data", {}),
            "channel_used": result.get("_channel_used", "sdk_control")
        }
    except Exception as e:
        logger.exception(f"moments/post 异常: {e}")
        return {
            "code": 200,
            "success": False,
            "error": str(e),
            "data": {},
            "channel_used": result.get("_channel_used", "sdk_control")
        }


@router.post("/moments/like", response_model=dict, tags=["朋友圈管理"])
async def like_moments(req: LikeMomentsRequest):
    """点赞朋友圈"""
    _check_device_online(req.device_id)
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "like_moments")
    if not guard["pass"]:
        return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}
    result = await _execute_skill(
        req.device_id, req.platform.value, "like_moments",
        {"user_id": req.user_id, "post_index": req.post_index}
    )
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/moments/comment", response_model=dict, tags=["朋友圈管理"])
async def comment_moments(req: CommentMomentsRequest):
    """评论朋友圈"""
    _check_device_online(req.device_id)
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "comment_moments", req.comment)
    if not guard["pass"]:
        return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}
    result = await _execute_skill(
        req.device_id, req.platform.value, "comment_moments",
        {
            "user_id": req.user_id,
            "post_index": req.post_index,
            "comment": guard.get("content", req.comment),
            "reply_to": req.reply_to
        }
    )
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


@router.post("/moments/list", response_model=dict, tags=["朋友圈管理"])
async def get_moments(req: GetMomentsRequest):
    """获取朋友圈列表"""
    
    _check_device_online(req.device_id)
    try:
        result = await asyncio.wait_for(
            _execute_skill(
                req.device_id, req.platform.value, "get_moments",
                {"user_id": req.user_id, "limit": req.limit},
                timeout=25,
                hook_only=req.platform == Platform.WECHAT,
            ),
            timeout=30,
        )
    except asyncio.TimeoutError:
        result = {
            "code": 503,
            "data": {
                "success": False,
                "error": "朋友圈列表读取超时：SnsMicroMsg.db/WCDB2 适配待完成",
                "blocked_reason": "sns_wcdb2_timeout",
                "moments": [],
                "count": 0,
            },
            "_channel_used": "websocket/hook(timeout)",
        }
    
    return {
        "code": 200,
        "data": result.get("data", {}),
        "channel_used": result.get("_channel_used", "sdk_control")
    }


# =============================================================================
# 六、消息高级操作
# =============================================================================

class ForwardMessageRequest(BaseModel):
    device_id: str
    platform: Platform
    to_id: str
    content: str = ""
    msg_svr_id: Optional[str] = None

class SendCardRequest(BaseModel):
    device_id: str
    platform: Platform
    to_id: str
    card_wxid: str

@router.post("/message/forward", response_model=dict, tags=["消息管理"])
async def forward_message(req: ForwardMessageRequest):
    """转发消息"""
    _check_device_online(req.device_id)
    params = {"to_id": req.to_id, "content": req.content}
    if req.msg_svr_id:
        params["msg_svr_id"] = req.msg_svr_id
    result = await _execute_skill(
        req.device_id, req.platform.value, "forward_message", params
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

class RecallMessageRequest(BaseModel):
    device_id: str
    platform: Platform = Platform.WECHAT
    msg_svr_id: Optional[str] = None

@router.post("/message/recall", response_model=dict, tags=["消息管理"])
async def recall_message(req: RecallMessageRequest):
    """撤回最近一条消息"""
    _check_device_online(req.device_id)
    params = {}
    if req.msg_svr_id:
        params["msg_svr_id"] = req.msg_svr_id
    result = await _execute_skill(req.device_id, req.platform.value, "recall_message", params)
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/message/send-card", response_model=dict, tags=["消息管理"])
async def send_card(req: SendCardRequest):
    """发送名片"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "send_card",
        {"to_id": req.to_id, "card_wxid": req.card_wxid}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 七、个人设置接口
# =============================================================================

class SetNicknameRequest(BaseModel):
    device_id: str
    platform: Platform
    nickname: str

class SetSignatureRequest(BaseModel):
    device_id: str
    platform: Platform
    signature: str

class SetAvatarRequest(BaseModel):
    device_id: str
    platform: Platform
    image_path: Optional[str] = None

class SetGenderRequest(BaseModel):
    device_id: str
    platform: Platform
    gender: str

class SetRegionRequest(BaseModel):
    device_id: str
    platform: Platform
    region: str

@router.get("/profile/get", response_model=dict, tags=["个人设置"])
async def get_profile(device_id: str, platform: Platform = Platform.WECHAT):
    """获取当前微信账号资料"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "get_profile", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/profile/set-nickname", response_model=dict, tags=["个人设置"])
async def set_nickname(req: SetNicknameRequest):
    """修改微信昵称"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_nickname",
        {"nickname": req.nickname}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/profile/set-signature", response_model=dict, tags=["个人设置"])
async def set_signature(req: SetSignatureRequest):
    """修改个性签名"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_signature",
        {"signature": req.signature}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/profile/set-avatar", response_model=dict, tags=["个人设置"])
async def set_avatar(req: SetAvatarRequest):
    """修改头像"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_avatar",
        {"image_path": req.image_path}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/profile/set-gender", response_model=dict, tags=["个人设置"])
async def set_gender(req: SetGenderRequest):
    """设置性别"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_gender",
        {"gender": req.gender}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/profile/set-region", response_model=dict, tags=["个人设置"])
async def set_region(req: SetRegionRequest):
    """设置地区"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_region",
        {"region": req.region}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 八、账号安全接口
# =============================================================================

class UnblockAccountRequest(BaseModel):
    device_id: str
    platform: Platform
    helper_wxid: Optional[str] = None

class ChangePasswordRequest(BaseModel):
    device_id: str
    platform: Platform
    old_pwd: str
    new_pwd: str

@router.get("/account/status", response_model=dict, tags=["账号安全"])
async def check_account_status(device_id: str, platform: Platform = Platform.WECHAT):
    """检查账号状态"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "check_account_status", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/account/unblock", response_model=dict, tags=["账号安全"])
async def unblock_account(req: UnblockAccountRequest):
    """微信解封"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "unblock_account",
        {"helper_wxid": req.helper_wxid}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.get("/account/safety-center", response_model=dict, tags=["账号安全"])
async def safety_center(device_id: str, platform: Platform):
    """打开微信安全中心"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "safety_center", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/account/change-password", response_model=dict, tags=["账号安全"])
async def change_password(req: ChangePasswordRequest):
    """修改微信密码"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "change_password",
        {"old_pwd": req.old_pwd, "new_pwd": req.new_pwd}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 九、收藏管理接口
# =============================================================================

class AddFavoriteRequest(BaseModel):
    device_id: str
    platform: Platform = Platform.WECHAT
    content_desc: Optional[str] = None
    content: Optional[str] = None
    content_type: Optional[str] = "text"
    msg_svr_id: Optional[str] = None

@router.post("/favorites/add", response_model=dict, tags=["收藏管理"])
async def add_favorite(req: AddFavoriteRequest):
    """收藏消息"""
    _check_device_online(req.device_id)
    params = {"type": req.content_type or "text"}
    if req.msg_svr_id:
        params["msg_svr_id"] = req.msg_svr_id
    if req.content:
        params["content"] = req.content
    if req.content_desc:
        params["content_desc"] = req.content_desc
    result = await _execute_skill(
        req.device_id, req.platform.value, "add_favorite", params
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.get("/favorites/list", response_model=dict, tags=["收藏管理"])
async def get_favorites(device_id: str, platform: Platform = Platform.WECHAT, limit: int = 20):
    """获取收藏列表"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "get_favorites", {"limit": limit})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十、聊天设置接口
# =============================================================================

class ChatSettingRequest(BaseModel):
    device_id: str
    platform: Platform
    user_id: str
    enable: bool = True

@router.post("/chat/set-top", response_model=dict, tags=["聊天设置"])
async def set_chat_top(req: ChatSettingRequest):
    """置顶/取消置顶聊天"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_chat_top",
        {"user_id": req.user_id, "enable": req.enable}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/chat/set-mute", response_model=dict, tags=["聊天设置"])
async def set_mute_chat(req: ChatSettingRequest):
    """消息免打扰"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "set_mute_chat",
        {"user_id": req.user_id, "enable": req.enable}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/chat/clear-history", response_model=dict, tags=["聊天设置"])
async def clear_chat_history(device_id: str, platform: Platform, user_id: str):
    """清空聊天记录"""
    _check_device_online(device_id)
    result = await _execute_skill(
        device_id, platform.value, "clear_chat_history",
        {"user_id": user_id}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十一、联系人搜索接口
# =============================================================================

@router.get("/contacts/search", response_model=dict, tags=["好友管理"])
async def search_contacts(device_id: str, keyword: str, platform: Platform = Platform.WECHAT):
    """搜索联系人"""
    _check_device_online(device_id)
    result = await _execute_skill(
        device_id, platform.value, "search_contact",
        {"keyword": keyword}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


@router.get("/customer/profile-bundle", response_model=dict, tags=["存客宝对接"])
async def get_customer_profile_bundle(
    device_id: str,
    platform: Platform = Platform.WECHAT,
    keyword: str = "",
    user_id: str = "",
    limit: int = 50,
    contact_limit: int = DEFAULT_CONTACT_PULL_LIMIT,
    message_limit: int = DEFAULT_MESSAGE_PULL_LIMIT,
    group_limit: int = 100,
    tag_limit: int = 100,
    contact_offset: int = 0,
    message_offset: int = 0,
    group_offset: int = 0,
    tag_offset: int = 0,
):
    """
    客户画像聚合包：资料、联系人、标签、群、最近消息一次返回。

    面向存客宝/超管 BFF，减少多接口拼装成本；底层仍走真机 Hook/Agent。
    """
    from services.wechat_contact_normalizer import normalize_wechat_contacts

    _check_device_online(device_id)
    limit = _bounded_limit(limit, 50, 500)
    contact_limit = _bounded_limit(contact_limit, DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT)
    message_limit = _bounded_limit(message_limit, DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
    group_limit = _bounded_limit(group_limit, 100, 500)
    tag_limit = _bounded_limit(tag_limit, 100, 500)
    contact_offset = _bounded_offset(contact_offset)
    message_offset = _bounded_offset(message_offset)
    group_offset = _bounded_offset(group_offset)
    tag_offset = _bounded_offset(tag_offset)
    query = user_id or keyword

    tasks = {
        "profile": _execute_skill(device_id, platform.value, "get_profile", {}, timeout=45),
        "contacts": _execute_skill(device_id, platform.value, "get_contacts", {"limit": contact_limit, "offset": contact_offset}, timeout=90),
        "groups": _execute_skill(device_id, platform.value, "get_groups", {"limit": group_limit, "offset": group_offset}, timeout=45),
        "tags": _execute_skill(device_id, platform.value, "get_tags", {"limit": tag_limit, "offset": tag_offset}, timeout=45),
        "messages": _execute_skill(device_id, platform.value, "get_messages", {"limit": message_limit, "offset": message_offset}, timeout=90),
    }
    if query:
        tasks["contact_search"] = _execute_skill(
            device_id, platform.value, "search_contact", {"keyword": query}, timeout=45
        )
    if user_id:
        tasks["friend_info"] = _execute_skill(
            device_id, platform.value, "get_friend_info", {"user_id": user_id}, timeout=45
        )

    names = list(tasks.keys())
    raw_results = await asyncio.gather(*tasks.values(), return_exceptions=True)
    results = {}
    channels = {}
    errors = {}
    for name, result in zip(names, raw_results):
        if isinstance(result, Exception):
            results[name] = {}
            errors[name] = str(result)[:200]
            channels[name] = "error"
            continue
        results[name] = _payload_of(result)
        channels[name] = result.get("_channel_used", "sdk_control") if isinstance(result, dict) else "unknown"

    contacts = _list_from_payload(results.get("contacts", {}), "contacts")
    contacts = normalize_wechat_contacts(contacts) if platform == Platform.WECHAT else contacts
    matched_contacts = [c for c in contacts if _matches_contact(c, query)][:limit] if query else contacts[:limit]
    groups = _list_from_payload(results.get("groups", {}), "groups", "chatrooms")
    tags = _list_from_payload(results.get("tags", {}), "tags", "labels")
    messages = _list_from_payload(results.get("messages", {}), "messages")[:limit]
    contact_payload = results.get("contacts", {})
    message_payload = results.get("messages", {})
    contact_total = contact_payload.get("total_count") or contact_payload.get("total") or (contact_payload.get("_diag") or {}).get("filtered_total") or len(contacts)
    message_items = _list_from_payload(message_payload, "messages")
    message_total = message_payload.get("total_count") or message_payload.get("total") or len(message_items)

    return {
        "code": 200,
        "data": {
            "device_id": device_id,
            "device_id_md5": device_id_md5(device_id),
            "query": {
                "keyword": keyword,
                "user_id": user_id,
                "limit": limit,
                "contact_limit": contact_limit,
                "message_limit": message_limit,
                "group_limit": group_limit,
                "tag_limit": tag_limit,
                "contact_offset": contact_offset,
                "message_offset": message_offset,
                "group_offset": group_offset,
                "tag_offset": tag_offset,
            },
            "profile": results.get("profile", {}),
            "contacts": matched_contacts,
            "contact_count": contact_total,
            "returned_contact_count": len(contacts),
            "matched_contact_count": len(matched_contacts),
            "contact_requested_limit": contact_limit,
            "contact_offset": contact_offset,
            "contact_total_count": contact_total,
            "contact_raw_total_count": (contact_payload.get("_diag") or {}).get("rcontact_total"),
            "contacts_has_more": bool(contact_payload.get("has_more")) or len(contacts) >= contact_limit,
            "groups": groups[:limit],
            "group_count": len(groups),
            "group_requested_limit": group_limit,
            "group_offset": group_offset,
            "tags": tags[:limit],
            "tag_count": len(tags),
            "tag_requested_limit": tag_limit,
            "tag_offset": tag_offset,
            "recent_messages": messages,
            "message_count": message_total,
            "returned_message_count": len(message_items),
            "message_requested_limit": message_limit,
            "message_offset": message_offset,
            "message_total_count": message_total,
            "messages_has_more": bool(message_payload.get("has_more")) or len(message_items) >= message_limit,
            "contact_search": results.get("contact_search", {}),
            "friend_info": results.get("friend_info", {}),
            "errors": errors,
        },
        "channel_used": channels,
    }

@router.get("/friend/info", response_model=dict, tags=["好友管理"])
async def get_friend_info(device_id: str, platform: Platform, user_id: str):
    """获取好友详细资料"""
    _check_device_online(device_id)
    result = await _execute_skill(
        device_id, platform.value, "get_friend_info",
        {"user_id": user_id}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十二、群聊高级操作
# =============================================================================

@router.post("/group/quit", response_model=dict, tags=["群聊管理"])
async def quit_group(device_id: str, platform: Platform, group_id: str):
    """退出群聊"""
    _check_device_online(device_id)
    result = await _execute_skill(
        device_id, platform.value, "quit_group",
        {"group_id": group_id}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十三、朋友圈高级操作
# =============================================================================

@router.post("/moments/delete", response_model=dict, tags=["朋友圈管理"])
async def delete_moments(device_id: str, platform: Platform, post_index: int = 0):
    """删除朋友圈"""
    _check_device_online(device_id)
    result = await _execute_skill(
        device_id, platform.value, "delete_moments",
        {"post_index": post_index}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十四、支付接口
# =============================================================================

class RedPacketRequest(BaseModel):
    device_id: str
    platform: Platform
    to_id: str
    amount: str
    message: str = "恭喜发财"

class TransferRequest(BaseModel):
    device_id: str
    platform: Platform
    to_id: str
    amount: str
    message: str = ""

@router.post("/payment/red-packet", response_model=dict, tags=["支付"])
async def send_red_packet(req: RedPacketRequest):
    """发红包"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "send_red_packet",
        {"to_id": req.to_id, "amount": req.amount, "message": req.message}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/payment/transfer", response_model=dict, tags=["支付"])
async def send_transfer(req: TransferRequest):
    """转账"""
    _check_device_online(req.device_id)
    result = await _execute_skill(
        req.device_id, req.platform.value, "transfer",
        {"to_id": req.to_id, "amount": req.amount, "message": req.message}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十五、小程序 & 公众号
# =============================================================================

@router.post("/miniprogram/open", response_model=dict, tags=["小程序"])
async def open_mini_program(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    name: str = Body(""),
    app_id: str = Body(""),
):
    """打开小程序"""
    _check_device_online(device_id)
    result = await _execute_skill(
        device_id, platform.value, "open_mini_program", {"name": name, "app_id": app_id or name}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/official-account/follow", response_model=dict, tags=["公众号"])
async def follow_official_account(device_id: str, platform: Platform, account_name: str):
    """关注公众号"""
    _check_device_online(device_id)
    result = await _execute_skill(
        device_id, platform.value, "follow_official_account",
        {"account_name": account_name}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十六、解封与限制管理
# =============================================================================

@router.post("/account/unblock-self", response_model=dict, tags=["账号安全"])
async def unblock_self(device_id: str, platform: Platform):
    """自助解封"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "unblock_self", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/account/unblock-appeal", response_model=dict, tags=["账号安全"])
async def unblock_appeal(device_id: str, platform: Platform):
    """申诉解封"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "unblock_appeal", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/account/unblock-customer-service", response_model=dict, tags=["账号安全"])
async def unblock_via_customer_service(
    device_id: str,
    platform: Platform,
    reason: str = "",
    phone: str = "",
    wxid: str = "",
    max_rounds: int = 20,
    chat_interval_sec: int = 8,
    use_web_search: bool = True,
):
    """
    联系客服解封 — 全自动链路 + AI 对话

    流程: 设置→账号与安全→微信安全中心→联系客服→客服会话→AI 持续软磨硬泡

    返回 data 含 `session.turns[]`（每轮客服消息+AI 回复+发送状态）与 `final_status`：
    success / reject / timeout / fallback / error
    """
    _check_device_online(device_id)
    params = {
        "reason": reason,
        "phone": phone,
        "wxid": wxid,
        "max_rounds": max_rounds,
        "chat_interval_sec": chat_interval_sec,
        "use_web_search": use_web_search,
    }
    run_timeout = max(180, int(max_rounds) * (int(chat_interval_sec) + 12) + 180)
    result = await _execute_skill(
        device_id,
        platform.value,
        "unblock_via_customer_service",
        params,
        timeout=run_timeout,
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.get("/account/restrictions", response_model=dict, tags=["账号安全"])
async def check_restrictions(device_id: str, platform: Platform):
    """检查当前功能限制"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "check_restrictions", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/account/appeal-restriction", response_model=dict, tags=["账号安全"])
async def appeal_restriction(device_id: str, platform: Platform):
    """申诉功能限制"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "appeal_restriction", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/account/unblock-sms", response_model=dict, tags=["账号安全"])
async def unblock_with_sms(device_id: str, platform: Platform, phone: str = ""):
    """短信验证解封"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "unblock_with_sms", {"phone": phone})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十七、视频号
# =============================================================================

@router.get("/video-channel/list", response_model=dict, tags=["视频号"])
async def get_video_list(device_id: str, platform: Platform = Platform.WECHAT, limit: int = 10):
    """获取视频号列表"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "get_video_list", {"limit": limit})
    payload = result.get("data") if result.get("data") is not None else result
    if platform == Platform.WECHAT and isinstance(payload, dict) and payload.get("success") is False:
        # 视频号尚无稳定内部 RPC；设备有 ADB 时使用真实 UI 读取兜底，禁止把广播占位返回成成功。
        adb_device = adb_manager.get_device(device_id)
        if adb_device and adb_device.is_online():
            adb_result = await _execute_via_adb(
                adb_device,
                platform.value,
                "get_video_list",
                {"limit": min(max(limit, 1), 20)},
                timeout=60,
            )
            if adb_result.get("success"):
                return {"code": 200, "data": adb_result, "channel_used": "adb/sdk_control"}
            payload = {
                **payload,
                "adb_fallback": adb_result,
            }
        return {"code": 503, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/video-channel/like", response_model=dict, tags=["视频号"])
async def like_video(device_id: str, platform: Platform, index: int = 0):
    """点赞视频"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "like_video", {"index": index})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/video-channel/comment", response_model=dict, tags=["视频号"])
async def comment_video(device_id: str, platform: Platform, index: int = 0, comment: str = ""):
    """评论视频"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "comment_video", {"index": index, "comment": comment})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/video-channel/follow", response_model=dict, tags=["视频号"])
async def follow_video_creator(device_id: str, platform: Platform, index: int = 0):
    """关注视频号创作者"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "follow_video_creator", {"index": index})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/video-channel/share", response_model=dict, tags=["视频号"])
async def share_video(device_id: str, platform: Platform, index: int = 0, to_id: str = ""):
    """分享视频"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "share_video", {"index": index, "to_id": to_id})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十八、扫一扫
# =============================================================================

@router.post("/scan/qr-code", response_model=dict, tags=["扫一扫"])
async def scan_qr_code(device_id: str, platform: Platform):
    """扫描二维码"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "scan_qr_code", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/scan/add-friend", response_model=dict, tags=["扫一扫"])
async def scan_add_friend(device_id: str, platform: Platform):
    """扫码加好友"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "scan_add_friend", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.get("/scan/my-qr", response_model=dict, tags=["扫一扫"])
async def show_my_qr(device_id: str, platform: Platform):
    """显示我的二维码"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "show_my_qr", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/scan/extract-qr", response_model=dict, tags=["扫一扫"])
async def extract_qr_from_image(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    image_base64: str = Body("", description="二维码图片 base64（PNG/JPG，可含 data: 前缀）"),
    image_url: str = Body("", description="二维码图片可下载 URL"),
):
    """从图片识别二维码（支持传入 image_base64 / image_url，缺省则识别当前相册选图页）"""
    _check_device_online(device_id)
    params = {}
    if image_base64:
        params["image_base64"] = image_base64
    if image_url:
        params["image_url"] = image_url
    result = await _execute_skill(device_id, platform.value, "extract_qr_from_image", params)
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


@router.post("/scan/add-friend-from-image", response_model=dict, tags=["扫一扫"])
async def add_friend_from_image(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    image_base64: str = Body("", description="好友二维码图片 base64（PNG/JPG，可含 data: 前缀）"),
    image_url: str = Body("", description="好友二维码图片可下载 URL"),
    verify_message: str = Body("", description="加好友打招呼语（可选）"),
):
    """AI/BFF 下发二维码图片 → 真机识别 → 自动加好友（WP-WX-04 04.1 契约层）。

    执行链（Agent 侧 add_friend_from_image）：保存图片到相册 → 微信扫一扫从相册选图
    → 识别 qr_content → add_friend_by_qr(verify_message)。
    失败诚实返回（识别失败/二维码过期/已是好友/503 Hook 未附着），禁止 mock success。
    """
    if not image_base64 and not image_url:
        raise HTTPException(status_code=400, detail="必须提供 image_base64 或 image_url 之一")
    _check_device_online(device_id)
    params = {"verify_message": verify_message}
    if image_base64:
        params["image_base64"] = image_base64
    if image_url:
        params["image_url"] = image_url
    result = await _execute_skill(device_id, platform.value, "add_friend_from_image", params)
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 十九、支付增强
# =============================================================================

@router.get("/payment/code", response_model=dict, tags=["支付"])
async def show_payment_code(device_id: str, platform: Platform):
    """显示付款码"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "show_payment_code", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/payment/receive", response_model=dict, tags=["支付"])
async def receive_payment(device_id: str, platform: Platform, amount: str = "", desc: str = ""):
    """收款"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "receive_payment", {"amount": amount, "desc": desc})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.get("/payment/wallet", response_model=dict, tags=["支付"])
async def view_wallet(device_id: str, platform: Platform):
    """查看钱包"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "view_wallet", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.get("/payment/transactions", response_model=dict, tags=["支付"])
async def view_transactions(device_id: str, platform: Platform, limit: int = 20):
    """查看账单"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "view_transactions", {"limit": limit})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/payment/receive-red-packet", response_model=dict, tags=["支付"])
async def receive_red_packet(device_id: str, platform: Platform, from_id: str = ""):
    """领取红包"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "receive_red_packet", {"from_id": from_id})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十、通话
# =============================================================================

@router.post("/call/voice", response_model=dict, tags=["通话"])
async def voice_call(device_id: str, platform: Platform, user_id: str = ""):
    """语音通话"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "voice_call", {"user_id": user_id})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/call/video", response_model=dict, tags=["通话"])
async def video_call(device_id: str, platform: Platform, user_id: str = ""):
    """视频通话"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "video_call", {"user_id": user_id})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十一、群发助手
# =============================================================================

class MassSendRequest(BaseModel):
    device_id: str
    platform: Platform
    content: str
    user_ids: List[str]

@router.post("/mass-send", response_model=dict, tags=["群发助手"])
async def mass_send(req: MassSendRequest):
    """群发消息"""
    _check_device_online(req.device_id)
    guard = await _anti_ban_guard(req.device_id, req.platform.value, "batch_send", req.content)
    if not guard["pass"]:
        return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked", "risk_action": guard.get("risk_action")}
    result = await _execute_skill(
        req.device_id, req.platform.value, "mass_send",
        {"content": guard.get("content", req.content), "user_ids": req.user_ids}
    )
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十二、搜一搜/看一看
# =============================================================================

@router.get("/search/wechat", response_model=dict, tags=["搜一搜"])
async def wechat_search(device_id: str, platform: Platform, keyword: str = ""):
    """搜一搜"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "wechat_search", {"keyword": keyword})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.get("/discover/top-stories", response_model=dict, tags=["看一看"])
async def top_stories(device_id: str, platform: Platform):
    """看一看"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "top_stories", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十三、微信运动
# =============================================================================

@router.get("/wechat-sport/steps", response_model=dict, tags=["微信运动"])
async def get_steps(device_id: str, platform: Platform = Platform.WECHAT):
    """获取步数"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "get_steps", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/wechat-sport/like-steps", response_model=dict, tags=["微信运动"])
async def like_steps(device_id: str, platform: Platform, user_id: str = ""):
    """点赞步数"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "like_steps", {"user_id": user_id})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十四、位置分享
# =============================================================================

@router.post("/location/send", response_model=dict, tags=["位置"])
async def send_location(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    to_id: str = Body(""),
    user_id: str = Body(""),
    latitude: float = Body(0),
    longitude: float = Body(0),
    name: str = Body(""),
    label: str = Body(""),
):
    """发送位置"""
    _check_device_online(device_id)
    tid = to_id or user_id
    result = await _execute_skill(device_id, platform.value, "send_location", {
        "to_id": tid, "latitude": latitude, "longitude": longitude,
        "label": label or name, "name": name,
    })
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/location/share-realtime", response_model=dict, tags=["位置"])
async def share_real_time_location(device_id: str, platform: Platform, user_id: str = ""):
    """共享实时位置"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "share_real_time_location", {"user_id": user_id})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十五、表情管理
# =============================================================================

@router.post("/emoji/send", response_model=dict, tags=["表情"])
async def send_emoji(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    to_id: str = Body(""),
    user_id: str = Body(""),
    emoji_name: str = Body(""),
    emoji_md5: str = Body(""),
):
    """发送表情"""
    _check_device_online(device_id)
    tid = to_id or user_id
    result = await _execute_skill(device_id, platform.value, "send_emoji", {
        "to_id": tid, "user_id": tid, "emoji_name": emoji_name, "emoji_md5": emoji_md5 or emoji_name,
    })
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.get("/emoji/stickers", response_model=dict, tags=["表情"])
async def get_sticker_list(device_id: str, platform: Platform = Platform.WECHAT):
    """获取表情包列表"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "get_sticker_list", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十六、朋友圈增强
# =============================================================================

@router.post("/moments/set-cover", response_model=dict, tags=["朋友圈管理"])
async def set_moments_cover(device_id: str, platform: Platform):
    """设置朋友圈封面"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "set_moments_cover", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/moments/set-privacy", response_model=dict, tags=["朋友圈管理"])
async def set_moments_privacy(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    privacy_type: str = Body("all"),
    days: int = Body(180),
):
    """设置朋友圈可见天数"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "set_moments_privacy", {
        "privacy_type": privacy_type,
        "days": days,
    })
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/moments/share-link", response_model=dict, tags=["朋友圈管理"])
async def share_link_to_moments(device_id: str, platform: Platform, url: str = "", title: str = ""):
    """分享链接到朋友圈"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "forward_moments_link", {"url": url, "title": title})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十七、语音消息
# =============================================================================

@router.post("/message/voice", response_model=dict, tags=["消息管理"])
async def send_voice_message(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    to_id: str = Body(""),
    user_id: str = Body(""),
    duration: int = Body(3),
    voice_path: str = Body(""),
):
    """发送语音消息"""
    _check_device_online(device_id)
    tid = to_id or user_id
    result = await _execute_skill(device_id, platform.value, "send_voice_message", {
        "to_id": tid, "user_id": tid, "duration": duration, "voice_path": voice_path,
    })
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十八、文件管理
# =============================================================================

@router.post("/file/send", response_model=dict, tags=["文件管理"])
async def send_file(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    to_id: str = Body(""),
    user_id: str = Body(""),
    file_path: str = Body(""),
):
    """发送文件"""
    _check_device_online(device_id)
    tid = to_id or user_id
    result = await _execute_skill(device_id, platform.value, "send_file", {
        "to_id": tid, "file_path": file_path,
    })
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/file/download", response_model=dict, tags=["文件管理"])
async def download_file(device_id: str, platform: Platform, user_id: str = ""):
    """下载文件"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "download_file", {"user_id": user_id})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 二十九、设置管理
# =============================================================================

@router.post("/settings/do-not-disturb", response_model=dict, tags=["设置"])
async def toggle_dnd(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
    enable: bool = Body(True),
):
    """勿扰模式"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "toggle_do_not_disturb", {"enable": enable})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/settings/clear-cache", response_model=dict, tags=["设置"])
async def clear_cache(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
):
    """清理缓存"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "clear_cache", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/settings/check-update", response_model=dict, tags=["设置"])
async def check_update(
    device_id: str = Body(...),
    platform: Platform = Body(Platform.WECHAT),
):
    """检查更新"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "check_for_update", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/settings/logout", response_model=dict, tags=["设置"])
async def logout(device_id: str, platform: Platform):
    """退出登录"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "logout", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}

@router.post("/settings/switch-account", response_model=dict, tags=["设置"])
async def switch_account(device_id: str, platform: Platform):
    """切换账号"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "switch_account", {})
    payload = result.get("data") if result.get("data") is not None else result
    return {"code": 200, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# =============================================================================
# 内部实现方法
# =============================================================================

async def _execute_actions_via_adb(device, actions: list) -> tuple:
    """
    通过 ADB 设备顺序执行 AI 解析出的 action 列表。
    返回 (success: bool, error: Optional[str])
    """
    import asyncio
    import time
    for item in actions:
        act = item.get("action") or item.get("action_type")
        params = item.get("params") or {}
        try:
            if act == "open_app":
                pkg = params.get("package", "")
                if pkg:
                    device.start_app(pkg)
                    await asyncio.sleep(2)
            elif act == "click":
                x, y = params.get("x", 0), params.get("y", 0)
                device.click(x, y)
                await asyncio.sleep(0.3)
            elif act == "swipe":
                x1, y1 = params.get("x1", 540), params.get("y1", 1500)
                x2, y2 = params.get("x2", 540), params.get("y2", 500)
                duration = params.get("duration", 300)
                device._shell(f"input swipe {x1} {y1} {x2} {y2} {duration}")
                await asyncio.sleep(0.3)
            elif act == "input_text":
                text = params.get("text", "")
                if text:
                    device.input_text(text)
                    await asyncio.sleep(0.2)
            elif act == "key_event":
                keycode = params.get("keycode", "KEYCODE_BACK")
                device._shell(f"input keyevent {keycode}")
                await asyncio.sleep(0.2)
            elif act == "back":
                device.press_key("back")
                await asyncio.sleep(0.2)
            elif act == "home":
                device.press_key("home")
                await asyncio.sleep(0.2)
            elif act == "wait":
                sec = params.get("seconds", 1)
                await asyncio.sleep(max(0.5, min(sec, 10)))
        except Exception as e:
            logger.warning(f"ADB 执行 action 失败: {act} {params} -> {e}")
            return False, str(e)
    return True, None


async def _send_via_official_api(req: SendMessageRequest) -> dict:
    """通过官方API发送（抖音/闲鱼等开放平台 API 对接后启用）"""
    platform_msg = {
        Platform.DOUYIN: "抖音开放平台私信 API 暂未对接",
        Platform.XIANYU: "闲鱼开放平台消息 API 暂未对接",
    }
    err = platform_msg.get(req.platform, "该平台官方API暂未实现")
    logger.info(f"官方API通道: {req.platform.value} -> {err}")
    return {"success": False, "error": err}


async def _send_via_hook(req: SendMessageRequest) -> dict:
    """
    Hook 通道发送 — 优先走 Frida RPC，失败自动降级到 SDK/ADB 通道

    路由策略：
    1. 检查设备是否有活跃的 Frida Hook 会话
    2. 有 → 通过 WebSocket 下发 hook_execute 指令（Agent 端 HookExecutor 执行）
    3. 无 → 降级到 _send_via_sdk（ADB/u2 通道）
    """
    script_id = (req.hook_config or {}).get("script_id")
    method = (req.hook_config or {}).get("method", "send_message")
    logger.info(f"[_send_via_hook] script_id={script_id} method={method} device_id={req.device_id}")

    mode = _get_device_mode(req.device_id)

    if mode == "websocket":
        try:
            hook_params = {
                "to_id": req.to_id,
                "content": req.content,
                "msg_type": req.msg_type.value if hasattr(req.msg_type, "value") else str(req.msg_type),
            }
            timeout = req.timeout_seconds or settings.MESSAGE_SEND_TIMEOUT
            result = await ws_hub.send_command(
                req.device_id,
                {
                    "type": "execute",
                    "data": {
                        "script": req.platform.value,
                        "action": "send_message",
                        "params": hook_params,
                        "hook_only": True,
                    },
                },
                timeout=timeout,
            )
            payload = result.get("data") if isinstance(result.get("data"), dict) else {}
            ok = result.get("code") == 200 and (
                payload.get("success") is True or result.get("success") is True
            )
            if ok:
                mid = payload.get("message_id") or result.get("message_id")
                return {
                    "success": True,
                    "message_id": mid,
                    "channel_used": "websocket/frida",
                }
            err = payload.get("error") or result.get("message") or "WebSocket Hook 发送失败"
            logger.warning(f"[_send_via_hook] WS Frida 失败: {result}")
            if getattr(settings, "WECHAT_WS_HOOK_ONLY", True):
                if _should_degrade_hook_send(err, result):
                    sdk_result = await _send_via_sdk(req)
                    sdk_result.setdefault(
                        "channel_used",
                        "websocket/hook(degraded_to_u2)" if sdk_result.get("success") else "websocket/hook+u2(failed)",
                    )
                    if not sdk_result.get("success"):
                        sdk_result.setdefault("hook_error", err)
                    return sdk_result
                return {"success": False, "error": err, "channel_used": "websocket/hook(failed)"}
        except Exception as e:
            logger.warning(f"[_send_via_hook] Hook 通道异常: {e}")
            if getattr(settings, "WECHAT_WS_HOOK_ONLY", True):
                return {"success": False, "error": str(e), "channel_used": "websocket/hook(failed)"}

    ws_hook_only = getattr(settings, "WECHAT_WS_HOOK_ONLY", True)
    if ws_hook_only:
        if mode != "websocket" or not ws_hub.is_online(req.device_id):
            return {
                "success": False,
                "error": "WebSocket Agent 未在线（WECHAT_WS_HOOK_ONLY）",
                "channel_used": "websocket/offline",
            }
    elif mode in ("adb", "websocket"):
        adb_device = adb_manager.get_device(req.device_id)
        device_serial = getattr(adb_device, "serial", req.device_id) if adb_device else req.device_id
        try:
            hook_result = await asyncio.get_running_loop().run_in_executor(
                None,
                lambda: _try_local_frida_hook(device_serial, req),
            )
            if hook_result and hook_result.get("success"):
                hook_result["channel_used"] = "hook"
                return hook_result
            logger.warning(f"[_send_via_hook] 本地 Frida 失败: {hook_result}")
            if getattr(settings, "WECHAT_BACKEND_ONLY", True):
                err = (hook_result or {}).get("error") or "Frida Hook 发送失败"
                return {"success": False, "error": err, "channel_used": "frida/hook(failed)"}
        except Exception as e:
            logger.warning(f"[_send_via_hook] 本地 Frida 异常: {e}")
            if getattr(settings, "WECHAT_BACKEND_ONLY", True):
                return {"success": False, "error": str(e), "channel_used": "frida/hook(failed)"}

    if getattr(settings, "WECHAT_BACKEND_ONLY", True) and not ws_hook_only:
        return {"success": False, "error": "微信后端模式要求 Frida Hook 可用", "channel_used": "none"}

    if ws_hook_only:
        return {
            "success": False,
            "error": "WebSocket Hook 发送失败",
            "channel_used": "websocket/hook(failed)",
        }

    result = await _send_via_sdk(req)
    if result.get("success"):
        result["channel_used"] = "hook(degraded_to_sdk)"
    return result


def _should_degrade_hook_send(error: str, result: dict) -> bool:
    """Hook 无法真实发送时，允许转到同一真机 Agent 的 u2 UI 兜底。"""
    text = f"{error or ''} {result or ''}"
    return any(
        token in text
        for token in (
            "no_receiver_registered",
            "接收端不可验证",
            "sendMessage",
            "RPC 返回 success=false",
        )
    )


# Frida 会话池：避免每条 API 反复 attach/detach 导致连跑验收失败
_frida_session_pool: dict = {}


def _load_phantom_frida_config() -> dict:
    """读取 anti_detect/phantom_frida_config.json（随机端口反检测 frida-server）"""
    import json
    import os
    base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    for rel in (
        os.path.join("scripts", "anti_detect", "phantom_frida_config.json"),
        os.path.join("sdk", "scripts", "anti_detect", "phantom_frida_config.json"),
    ):
        path = os.path.join(base_dir, rel)
        if os.path.isfile(path):
            try:
                with open(path, encoding="utf-8") as f:
                    cfg = json.load(f)
                if cfg.get("listen_port"):
                    return cfg
            except Exception:
                pass
    return {}


def _get_frida_manager(device_serial: str):
    """获取 FridaManager：优先 phantom/remote 随机端口，否则 usb"""
    import sys, os, subprocess
    base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    candidate_dirs = [
        os.path.join(base_dir, "agent"),
        os.path.join(base_dir, "sdk", "agent"),
    ]
    for agent_dir in candidate_dirs:
        if os.path.isdir(agent_dir) and agent_dir not in sys.path:
            sys.path.insert(0, agent_dir)
    from hook.frida_manager import FridaManager

    phantom = _load_phantom_frida_config()
    if phantom.get("listen_port"):
        port = int(phantom["listen_port"])
        adb_serial = phantom.get("device_serial") or device_serial
        try:
            subprocess.run(
                ["adb", "-s", adb_serial, "forward", f"tcp:{port}", f"tcp:{port}"],
                capture_output=True, timeout=5, check=False,
            )
        except Exception:
            pass
        return FridaManager(
            device_serial=adb_serial,
            mode="remote",
            gadget_host="127.0.0.1",
            gadget_port=port,
        )

    return FridaManager(device_serial=device_serial, mode="usb")


def _probe_device_frida(device_serial: str) -> dict:
    """探测设备 Frida Hook 能力：连接 → ping → 版本 → profile"""
    import subprocess
    probe = {
        "supports_hook": False,
        "frida_version": "",
        "root_status": False,
        "wechat_version": "",
        "profile": {},
        "hook_tests": {},
    }

    try:
        r = subprocess.run(
            ["adb", "-s", device_serial, "shell", "su", "-c", "id"],
            capture_output=True, text=True, timeout=5,
        )
        probe["root_status"] = "uid=0" in (r.stdout or "")
    except Exception:
        pass

    try:
        mgr = _get_frida_manager(device_serial)
        if not mgr.start():
            probe["hook_tests"]["connect"] = "failed"
            return probe

        probe["hook_tests"]["connect"] = "ok"
        rpc = mgr.rpc

        try:
            pong = rpc.ping()
            probe["hook_tests"]["ping"] = str(pong)[:200]
            probe["supports_hook"] = True
        except Exception as e:
            probe["hook_tests"]["ping"] = str(e)[:200]

        try:
            ver = rpc.get_wechat_version()
            probe["wechat_version"] = str(ver)
            probe["hook_tests"]["wechat_version"] = str(ver)
        except Exception as e:
            probe["hook_tests"]["wechat_version"] = str(e)[:200]

        try:
            profile = rpc.get_profile({})
            if isinstance(profile, dict):
                probe["profile"] = profile.get("data", profile)
            probe["hook_tests"]["profile"] = "ok"
        except Exception as e:
            probe["hook_tests"]["profile"] = str(e)[:200]

        try:
            import frida
            probe["frida_version"] = frida.__version__
        except Exception:
            pass

        mgr.stop()
    except ImportError:
        probe["hook_tests"]["connect"] = "frida not installed"
    except Exception as e:
        probe["hook_tests"]["connect"] = str(e)[:200]

    return probe


async def _fetch_hook_data_via_ws(device_id: str, modules: list, limits: dict = None) -> dict:
    """通过在线 Agent WebSocket 批量获取 Hook 数据。"""
    limits = limits or {}
    contact_limit = _bounded_limit(limits.get("contact_limit"), DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT)
    message_limit = _bounded_limit(limits.get("message_limit"), DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
    contact_offset = _bounded_offset(limits.get("contact_offset"))
    message_offset = _bounded_offset(limits.get("message_offset"))
    module_map = {
        "profile": ("get_profile", {}),
        "contacts": ("get_contacts", {"limit": contact_limit, "offset": contact_offset}),
        "groups": ("get_groups", {}),
        "messages": ("get_messages", {"limit": message_limit, "offset": message_offset}),
        "labels": ("get_tags", {}),
        "tags": ("get_tags", {}),
        "moments": ("get_moments", {"limit": 10}),
        "accounts": ("get_official_accounts", {}),
        "hook_status": ("get_hook_status", {}),
        "device_info": ("get_device_info", {}),
        "wechat_version": ("get_wechat_version", {}),
        "process_info": ("get_process_info", {}),
        "storage_info": ("get_storage_info", {}),
        "network_info": ("get_network_info", {}),
        "login_state": ("check_login_state", {}),
        "favorites": ("get_favorites", {"limit": 20}),
    }

    async def fetch_one(module: str):
        action_spec = module_map.get(module)
        if not action_spec:
            return module, {"success": False, "error": f"未知模块: {module}"}, False
        action, params = action_spec
        try:
            result = await _execute_skill(
                device_id,
                "wechat",
                action,
                params,
                timeout=45,
                hook_only=True,
            )
            payload = result.get("data") if isinstance(result.get("data"), dict) else result
            payload = payload if isinstance(payload, dict) else {"data": payload}
            ok = result.get("code") == 200 and payload.get("success") is not False
            if payload.get("success") is True or result.get("success") is True:
                ok = True
            payload.setdefault("action", action)
            payload.setdefault("channel_used", result.get("_channel_used") or result.get("channel") or "websocket/hook")
            return module, payload, ok
        except Exception as exc:
            return module, {"success": False, "error": str(exc)[:200], "action": action}, False

    data = {}
    ok_modules = []
    requested = modules or list(module_map.keys())
    results = await asyncio.gather(*(fetch_one(module) for module in requested))
    for module, payload, ok in results:
        data[module] = payload
        if ok:
            ok_modules.append(module)

    return {
        "success": bool(ok_modules),
        "data": data,
        "modules_fetched": ok_modules,
        "modules_requested": requested,
        "limits": {
            "contact_limit": contact_limit,
            "message_limit": message_limit,
            "contact_offset": contact_offset,
            "message_offset": message_offset,
        },
        "transport": "websocket",
        "channel_used": "websocket/hook",
        "partial_success": 0 < len(ok_modules) < len(requested),
    }


def _fetch_hook_data(device_serial: str, modules: list, limits: dict = None) -> dict:
    """通过本地 Frida 批量获取 Hook 数据"""
    import re
    def to_snake(name):
        return re.sub(r'(?<=[a-z0-9])([A-Z])', r'_\1', name).lower()

    limits = limits or {}
    contact_limit = _bounded_limit(limits.get("contact_limit"), DEFAULT_CONTACT_PULL_LIMIT, MAX_CONTACT_PULL_LIMIT)
    message_limit = _bounded_limit(limits.get("message_limit"), DEFAULT_MESSAGE_PULL_LIMIT, MAX_MESSAGE_PULL_LIMIT)
    contact_offset = _bounded_offset(limits.get("contact_offset"))
    message_offset = _bounded_offset(limits.get("message_offset"))
    MODULE_MAP = {
        "profile":      ("getProfile", {}),
        "contacts":     ("getContacts", {"limit": contact_limit, "offset": contact_offset}),
        "groups":       ("getGroups", {}),
        "messages":     ("getMessages", {"limit": message_limit, "offset": message_offset}),
        "labels":       ("getLabels", {}),
        "moments":      ("getMoments", {"limit": 10}),
        "accounts":     ("getOfficialAccounts", {}),
        "hook_status":  ("getHookStatus", {}),
        "device_info":  ("getDeviceInfo", None),
        "wechat_version": ("getWechatVersion", None),
        "process_info": ("getProcessInfo", {}),
        "storage_info": ("getStorageInfo", {}),
        "network_info": ("getNetworkInfo", {}),
        "login_state":  ("checkLoginState", {}),
        "favorites":    ("getFavorites", {"limit": 20}),
    }

    data = {}
    try:
        mgr = _get_frida_manager(device_serial)
        if not mgr.start():
            return {"success": False, "error": "Frida 连接失败", "data": {}}

        rpc = mgr.rpc
        for mod in modules:
            if mod not in MODULE_MAP:
                data[mod] = {"error": f"未知模块: {mod}"}
                continue
            camel_name, params = MODULE_MAP[mod]
            snake_name = to_snake(camel_name)
            try:
                fn = getattr(rpc, snake_name, None)
                if fn is None:
                    data[mod] = {"error": f"RPC 方法不存在: {snake_name}"}
                    continue
                resp = fn(params) if params is not None else fn()
                data[mod] = resp if isinstance(resp, dict) else {"data": resp}
            except Exception as e:
                data[mod] = {"error": str(e)[:200]}

        mgr.stop()
        return {
            "success": True,
            "data": data,
            "modules_fetched": list(data.keys()),
            "limits": {
                "contact_limit": contact_limit,
                "message_limit": message_limit,
                "contact_offset": contact_offset,
                "message_offset": message_offset,
            },
        }
    except ImportError:
        return {"success": False, "error": "frida 未安装", "data": {}}
    except Exception as e:
        return {"success": False, "error": str(e)[:200], "data": data}


def _get_frida_session(device_serial: str):
    """获取或复用已连接的 FridaManager（矩阵/E2E 连跑稳定性）"""
    import time
    mgr = _frida_session_pool.get(device_serial)
    if mgr is not None and getattr(mgr, "connected", False):
        return mgr
    for attempt in range(3):
        mgr = _get_frida_manager(device_serial)
        if mgr.start():
            _frida_session_pool[device_serial] = mgr
            return mgr
        time.sleep(0.8 * (attempt + 1))
        _frida_session_pool.pop(device_serial, None)
    return None


def _try_local_frida_action(device_serial: str, action: str, params: dict) -> dict:
    """任意 action 走本地 Frida + HookExecutor。action 名与 hook_executor.ACTION_TO_RPC 一致。"""
    try:
        mgr = _get_frida_session(device_serial)
        if not mgr:
            return {"success": False, "error": "Frida 连接失败"}
        import sys, os
        agent_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "agent")
        if agent_dir not in sys.path:
            sys.path.insert(0, agent_dir)
        from hook.hook_executor import HookExecutor
        executor = HookExecutor(mgr)
        norm = dict(params or {})
        if action == "send_message" and "msg_type" not in norm:
            norm["msg_type"] = "text"
        result = executor.execute(action, norm)
        return result
    except ImportError:
        return {"success": False, "error": "frida 未安装"}
    except Exception as e:
        _frida_session_pool.pop(device_serial, None)
        return {"success": False, "error": str(e)}


def _try_local_frida_hook(device_serial: str, req) -> dict:
    """尝试通过本地 Frida 直连设备执行 Hook（ADB 模式专用）"""
    try:
        mgr = _get_frida_session(device_serial)
        if not mgr:
            return {"success": False, "error": "Frida 连接失败"}

        import sys, os
        agent_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "agent")
        if agent_dir not in sys.path:
            sys.path.insert(0, agent_dir)
        from hook.hook_executor import HookExecutor

        executor = HookExecutor(mgr)
        result = executor.execute("send_message", {
            "to_id": req.to_id,
            "content": req.content,
            "msg_type": req.msg_type.value if hasattr(req.msg_type, "value") else str(req.msg_type),
        })
        return result
    except ImportError:
        return {"success": False, "error": "frida 未安装"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _send_via_sdk(req: SendMessageRequest) -> dict:
    """通过SDK控制发送（WebSocket或ADB）；超时由请求 timeout_seconds 或 MESSAGE_SEND_TIMEOUT 控制"""
    params = {
        "to_id": req.to_id,
        "content": req.content,
        "msg_type": req.msg_type.value
    }
    if req.media_url:
        params["media_url"] = req.media_url
    if req.at_list:
        params["at_list"] = req.at_list

    timeout = req.timeout_seconds if req.timeout_seconds is not None and req.timeout_seconds > 0 else (getattr(settings, "MESSAGE_SEND_TIMEOUT", 60) or 60)
    mode = _get_device_mode(req.device_id)
    forced = (req.channel or "").strip().lower()
    # 显式 sdk_control 时优先走 ADB 引擎（避免僵尸 WS 占通道导致 60s 超时）
    if forced == "sdk_control":
        adb_dev = adb_manager.get_device(req.device_id)
        if adb_dev and adb_dev.is_online():
            mode = "adb"
            timeout = max(timeout, 90)

    if mode == "websocket":
        logger.info(f"[_send_via_sdk] 下发 execute send_message timeout={timeout}s")
        result = await ws_hub.send_command(req.device_id, {
            "type": "execute",
            "data": {
                "script": req.platform.value,
                "action": "send_message",
                "params": params
            }
        }, timeout=timeout)

        payload = result.get("data") if isinstance(result.get("data"), dict) else {}
        success = payload.get("success") is True or result.get("success") is True
        if result.get("code") == 200 and success:
            return {
                "success": True,
                "message_id": payload.get("message_id") or result.get("message_id"),
                "channel_used": f"websocket/{result.get('channel') or payload.get('channel') or 'u2'}",
            }
        err = payload.get("error") or payload.get("frida_error") or result.get("message", "发送失败")
        if result.get("code") == 408:
            err = "timeout"
            logger.warning(f"[_send_via_sdk] 设备响应超时 device_id={req.device_id} to_id={req.to_id}")
        return {
            "success": False,
            "error": err,
            "channel_used": f"websocket/{result.get('channel') or payload.get('channel') or 'failed'}",
        }

    elif mode == "adb":
        adb_device = adb_manager.get_device(req.device_id)
        if not adb_device:
            return {"success": False, "error": "ADB设备不可用"}
        return await _execute_via_adb(adb_device, req.platform.value, "send_message", params)

    return {"success": False, "error": "设备不在线"}


async def _send_via_agent(req: SendMessageRequest) -> dict:
    """通过AI Agent发送：WebSocket 下发 agent_execute；ADB 模式由服务端解析后执行 action 序列"""
    platform_names = {
        Platform.WECHAT: "微信",
        Platform.DOUYIN: "抖音",
        Platform.XHS: "小红书",
        Platform.XIANYU: "闲鱼",
        Platform.SOUL: "Soul"
    }
    task = f"打开{platform_names.get(req.platform, req.platform.value)}，找到联系人'{req.to_id}'，发送消息：{req.content}"
    mode = _get_device_mode(req.device_id)

    if mode == "websocket":
        # 设备端已有 agent_execute 处理，直接下发任务
        result = await ws_hub.send_command(req.device_id, {
            "type": "agent_execute",
            "data": {"task": task, "llm_provider": "deepseek", "max_steps": 30}
        }, timeout=120)
        if result.get("code") == 200 and result.get("data"):
            return {
                "success": True,
                "message_id": result.get("data", {}).get("message_id") or f"agent_{int(__import__('time').time() * 1000)}"
            }
        return {"success": False, "error": result.get("message", "AI Agent 执行失败")}

    if mode == "adb":
        # ADB 模式：服务端解析意图后通过 ADB 执行 action 序列
        parse_result = await ai_agent_service.process_voice_command(task, req.device_id)
        if not parse_result.get("success") or not parse_result.get("actions"):
            return {"success": False, "error": parse_result.get("message", "无法解析指令")}
        adb_device = adb_manager.get_device(req.device_id)
        if not adb_device or not adb_device.is_online():
            return {"success": False, "error": "ADB 设备不可用"}
        ok, err = await _execute_actions_via_adb(adb_device, parse_result["actions"])
        if ok:
            return {"success": True, "message_id": f"agent_adb_{int(__import__('time').time() * 1000)}"}
        return {"success": False, "error": err or "ADB 执行失败"}

    return {"success": False, "error": "设备离线"}


# =============================================================================
# 三十、自动注册 & 设备初始化
# =============================================================================

class AutoRegisterRequest(BaseModel):
    """自动注册请求"""
    device_id: str
    nickname: str = "卡若AI"
    password: str = ""
    test_msg_to: str = ""
    test_msg_content: str = "你好，我是卡若AI工作手机"


class CheckLoginStateRequest(BaseModel):
    """检查登录状态请求"""
    device_id: str


class GetSimPhoneRequest(BaseModel):
    """获取 SIM 卡手机号请求"""
    device_id: str


@router.post("/auto-register/full", response_model=dict, tags=["自动注册"])
async def auto_register_full(req: AutoRegisterRequest):
    """
    全自动微信注册 — 一键完成

    流程:
    1. 检测微信登录状态
    2. 未登录 → 自动从 SIM 获取手机号 → 注册
    3. 自动读取短信验证码 → 填写
    4. 设置昵称/密码 → 完成注册
    5. 可选：发送测试消息
    """
    from services.auto_register import run_auto_register
    result = await run_auto_register(
        device_id=req.device_id,
        nickname=req.nickname,
        password=req.password,
        test_msg_to=req.test_msg_to,
        test_msg_content=req.test_msg_content,
    )
    return {"code": 200, "data": result}


@router.post("/auto-register/check-state", response_model=dict, tags=["自动注册"])
async def check_wechat_login_state(req: CheckLoginStateRequest):
    """检查微信登录状态（已登录/登录页/注册页/未安装）"""
    from services.auto_register import WeChatAutoRegister
    from services.adb_device import adb_manager

    adb_device = adb_manager.get_device(req.device_id)
    if not adb_device:
        return {"code": 503, "data": {"error": f"设备不可用: {req.device_id}"}}

    engine = WeChatAutoRegister(adb_device)
    loop = asyncio.get_running_loop()
    state = await loop.run_in_executor(None, engine.check_wechat_login_state)
    return {"code": 200, "data": state}


@router.post("/auto-register/get-sim-phone", response_model=dict, tags=["自动注册"])
async def get_sim_phone(req: GetSimPhoneRequest):
    """从设备 SIM 卡获取手机号"""
    from services.auto_register import WeChatAutoRegister
    from services.adb_device import adb_manager

    adb_device = adb_manager.get_device(req.device_id)
    if not adb_device:
        return {"code": 503, "data": {"error": f"设备不可用: {req.device_id}"}}

    engine = WeChatAutoRegister(adb_device)
    loop = asyncio.get_running_loop()
    sim_info = await loop.run_in_executor(None, engine.get_sim_phone_number)
    return {"code": 200, "data": sim_info}


@router.post("/account/wechat/check-login-state", response_model=dict, tags=["注册/登录"])
async def wechat_check_login_state_agent(device_id: str, platform: Platform):
    """设备端 u2 检测微信是否已登录"""
    _check_device_online(device_id)
    result = await _execute_skill(device_id, platform.value, "check_login_state", {})
    payload = result.get("data") if isinstance(result.get("data"), dict) and result.get("data") else result
    code = result.get("code", 200)
    return {"code": code, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


@router.post("/account/wechat/login-by-password", response_model=dict, tags=["注册/登录"])
async def wechat_login_by_password(
    device_id: str,
    platform: Platform,
    account: str = "",
    password: str = "",
    phone: str = "",
):
    """微信号/手机号 + 密码登录（u2 自动化）"""
    _check_device_online(device_id)
    params = {
        "account": account or phone,
        "phone": phone,
        "password": password,
    }
    result = await _execute_skill(
        device_id, platform.value, "login_by_password", params, timeout=120
    )
    payload = result.get("data") if isinstance(result.get("data"), dict) and result.get("data") else result
    code = result.get("code", 200)
    return {"code": code, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


@router.post("/account/wechat/ensure-login", response_model=dict, tags=["注册/登录"])
async def wechat_ensure_login(
    device_id: str,
    platform: Platform,
    account: str = "",
    password: str = "",
    phone: str = "",
):
    """未登录则按参数或 sdk/config/wechat_login.yaml 自动登录"""
    _check_device_online(device_id)
    params = {
        "account": account or phone,
        "phone": phone,
        "password": password,
    }
    result = await _execute_skill(
        device_id, platform.value, "ensure_logged_in", params, timeout=120
    )
    payload = result.get("data") if isinstance(result.get("data"), dict) and result.get("data") else result
    code = result.get("code", 200)
    return {"code": code, "data": payload, "channel_used": result.get("_channel_used", "sdk_control")}


# ========== AF12: 风控中心监控 ==========


@router.get("/anti-ban/dashboard")
async def anti_ban_dashboard():
    """防封风控中心：汇总全局限流/账号生命周期/指纹碰撞状态"""
    import time as _time
    from services.rate_limiter import rate_limiter as _rl

    try:
        devices = await device_manager.get_all_devices(limit=500)
    except Exception:
        devices = []

    online_ws = {d["device_id"]: d for d in ws_hub.get_online_devices()}
    from services.adb_device import adb_manager as _adb
    adb_serials = _adb.scan_devices()
    seen = {d.get("device_id") for d in devices}
    for did, info in online_ws.items():
        if did not in seen:
            devices.append({**info, "status": "online"})
            seen.add(did)
    for serial in adb_serials:
        if serial not in seen:
            devices.append({"device_id": serial, "status": "adb"})
            seen.add(serial)

    total_devices = len(devices)
    online = sum(1 for d in devices if d.get("status") in ("online", "adb"))

    seen_fp: dict = {}
    fp_collision_devices = []
    for d in devices:
        fp = d.get("fingerprint_hash", "")
        if not fp:
            continue
        did = d.get("device_id", "")
        seen_fp.setdefault(fp, []).append(did)
    for fp, dids in seen_fp.items():
        if len(dids) > 1:
            for did in dids:
                fp_collision_devices.append({
                    "device_id": did,
                    "collided_with": [x for x in dids if x != did],
                })

    lifecycle_summary = []
    for d in devices[:50]:
        did = d.get("device_id", "")
        plat = d.get("platform", "wechat")
        try:
            phase_info = await account_lifecycle.get_phase(did, plat)
            rules = await account_lifecycle.get_rules(did, plat)
        except Exception:
            phase_info = "unknown"
            rules = {}
        lifecycle_summary.append({
            "device_id": did,
            "phase": phase_info.value if hasattr(phase_info, "value") else str(phase_info),
            "max_daily_add_friend": rules.get("max_daily_add_friend", "N/A"),
            "max_daily_message": rules.get("max_daily_send_message", "N/A"),
        })

    return {
        "timestamp": int(_time.time()),
        "devices": {"total": total_devices, "online": online},
        "fingerprint_collisions": fp_collision_devices,
        "account_lifecycle": lifecycle_summary,
    }


@router.get("/anti-ban/device/{device_id}")
async def anti_ban_device_detail(device_id: str):
    """单设备防封详情"""
    device = await device_manager.get_device(device_id)
    if not device:
        raise HTTPException(status_code=404, detail="设备不存在")

    plat = device.get("platform", "wechat")
    phase = await account_lifecycle.get_phase(device_id, plat)
    rules = await account_lifecycle.get_rules(device_id, plat)
    fp = device.get("fingerprint_hash", "")

    collision_result = {"collision": False, "collided_with": []}
    if fp and device_manager.db:
        collision_result = await device_manager.check_fingerprint_collision(
            device_id, device.get("fingerprint_info", {})
        )

    return {
        "device_id": device_id,
        "status": device.get("status", "unknown"),
        "fingerprint_hash": fp,
        "fingerprint_collision": collision_result,
        "account_phase": phase.value if hasattr(phase, "value") else str(phase),
        "phase_rules": rules,
        "last_heartbeat": str(device.get("last_heartbeat", "")),
    }
