"""
skills/chromedev_mcp.py - ChromeDev MCP 浏览器自动化技能
=========================================================
通过 chrome-devtools-mcp (Google 官方 MCP Server) 提供浏览器自动化能力。

架构:
  本地 MCP Client (Python) ──stdio──> chrome-devtools-mcp (Node.js) ──CDP──> Chrome/Chromium

依赖:
  - Node.js >= 20.19 (用于运行 chrome-devtools-mcp)
  - chrome-devtools-mcp (npm 包, 首次使用自动安装)

工具映射 (myagent skill → MCP tool):
  - browser_open     → navigate_page + take_snapshot
  - browser_click    → click
  - browser_fill     → fill
  - browser_screenshot → take_screenshot
  - browser_eval     → evaluate_script
  - browser_navigate → close_page / list_pages / select_page / new_page
  - browser_close    → close_page

参考:
  https://github.com/ChromeDevTools/chrome-devtools-mcp
  https://www.npmjs.com/package/chrome-devtools-mcp
"""
from __future__ import annotations

import asyncio
import json
import os
import shutil
import subprocess
import sys
import time
from typing import Any, Dict, List, Optional

from core.logger import get_logger
from aiskills.base import Skill, SkillResult, SkillParameter

logger = get_logger("myagent.skills.chromedev_mcp")


# ── Xvfb 虚拟显示管理 ──────────────────────────────────────

_xvfb_process: Optional[subprocess.Popen] = None
_xvfb_display: Optional[str] = None


def _has_display() -> bool:
    """检查当前环境是否有可用的 X11 显示"""
    display = os.environ.get("DISPLAY", "").strip()
    if not display:
        return False
    # DISPLAY 已设置，尝试检测是否真的可用
    # 方法1: xdpyinfo
    try:
        result = subprocess.run(
            ["xdpyinfo"], capture_output=True, timeout=3,
            env={**os.environ},
        )
        if result.returncode == 0:
            return True
    except Exception:
        pass
    # 方法2: 用 xprop/xdotool 等轻量工具验证
    try:
        for cmd in (["xprop", "-root", "_NET_SUPPORTING_WM_CHECK"],
                    ["xdotool", "getdisplaywidth"]):
            result = subprocess.run(
                cmd, capture_output=True, timeout=3,
                env={**os.environ},
            )
            if result.returncode == 0:
                return True
    except Exception:
        pass
    # 方法3: 检查 /tmp/.X11-unix/ 下的 socket 文件
    try:
        if display.startswith(":"):
            num = display[1:].split(".")[0]
            socket_path = f"/tmp/.X11-unix/X{num}"
            if os.path.exists(socket_path):
                return True
    except Exception:
        pass
    return False


def _start_xvfb(display_num: int = 98) -> Optional[str]:
    """
    启动 Xvfb 虚拟显示服务器。

    Returns:
        DISPLAY 环境变量值（如 :98），失败返回 None
    """
    global _xvfb_process, _xvfb_display

    # 如果已经启动，直接返回
    if _xvfb_process and _xvfb_process.poll() is None:
        return _xvfb_display

    # 清理旧的
    if _xvfb_process:
        try:
            _xvfb_process.terminate()
            _xvfb_process.wait(timeout=3)
        except Exception:
            try:
                _xvfb_process.kill()
            except Exception:
                pass
        _xvfb_process = None

    xvfb_path = shutil.which("Xvfb")
    if not xvfb_path:
        logger.warning("Xvfb 未安装，无法启动虚拟显示")
        return None

    display_str = f":{display_num}"
    try:
        # 清理可能残留的 X11 锁文件
        for lock_file in (f"/tmp/.X{display_num}-lock", f"/tmp/.X11-unix/X{display_num}"):
            if os.path.exists(lock_file):
                try:
                    os.unlink(lock_file)
                except Exception:
                    pass

        _xvfb_process = subprocess.Popen(
            [xvfb_path, display_str, "-screen", "0", "1920x1080x24", "-ac", "-nolisten", "tcp"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        time.sleep(0.5)
        if _xvfb_process.poll() is not None:
            logger.error(f"Xvfb 启动后立即退出 (returncode={_xvfb_process.returncode})")
            _xvfb_process = None
            return None

        _xvfb_display = display_str
        os.environ["DISPLAY"] = display_str
        logger.info(f"Xvfb 虚拟显示已启动 (chromedev_mcp): {display_str}")
        return display_str
    except Exception as e:
        logger.error(f"启动 Xvfb 失败: {e}")
        _xvfb_process = None
        return None


def _stop_xvfb() -> None:
    """停止 Xvfb 虚拟显示服务器"""
    global _xvfb_process, _xvfb_display

    if _xvfb_process:
        try:
            _xvfb_process.terminate()
            _xvfb_process.wait(timeout=5)
        except Exception:
            try:
                _xvfb_process.kill()
            except Exception:
                pass
        _xvfb_process = None
        _xvfb_display = None
        logger.info("Xvfb 虚拟显示已停止 (chromedev_mcp)")


def _ensure_display() -> Optional[str]:
    """
    确保有可用的 X11 显示，供浏览器有头模式使用。

    优先级:
      1. 复用 VNC 远程桌面的 Xvfb 显示（用户可在 VNC 中看到浏览器操作）
      2. 自动启动 VNC（含 Xvfb + x11vnc + websockify）
      3. VNC 不可用时独立启动 Xvfb

    Returns:
        DISPLAY 值（如 ":99"），失败返回 None
    """
    # 1. 尝试复用 VNC 远程桌面
    try:
        from core.vnc_manager import get_vnc_manager
        vnc = get_vnc_manager()

        # VNC 已在运行 → 直接复用其 DISPLAY
        if vnc.is_running:
            display = vnc.display  # 默认 :99
            os.environ["DISPLAY"] = display
            logger.info(f"VNC 远程桌面已在运行，复用显示: {display}")
            return display

        # [v1.47.13] VNC 未运行 → 尝试自动启动
        # 修复: 不能在 ThreadPoolExecutor + asyncio.run 中直接调用 vnc.start()！
        # 因为 vnc.start() 内部用 asyncio.Lock()，跨 event loop 会导致死锁。
        # 正确做法：在单独线程中用 asyncio.run 启动，然后轮询等待 VNC 就绪。
        try:
            import asyncio
            import concurrent.futures

            def _start_vnc_in_thread():
                try:
                    return asyncio.run(vnc.start())
                except Exception as e:
                    return {"success": False, "message": str(e)}

            with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
                future = pool.submit(_start_vnc_in_thread)

                max_wait = 60
                check_interval = 2
                elapsed = 0
                while elapsed < max_wait:
                    if vnc.is_running:
                        display = vnc.display
                        os.environ["DISPLAY"] = display
                        logger.info(f"VNC 远程桌面已启动，复用显示: {display}")
                        return display

                    try:
                        result = future.result(timeout=0.1)
                        if result.get("success") or vnc.is_running:
                            display = vnc.display
                            os.environ["DISPLAY"] = display
                            logger.info(f"VNC 远程桌面已启动，显示: {display}")
                            return display
                        else:
                            logger.warning(f"VNC 启动失败: {result.get('message', '')}")
                            break
                    except concurrent.futures.TimeoutError:
                        pass

                    import time
                    time.sleep(check_interval)
                    elapsed += check_interval

                if vnc.is_running:
                    display = vnc.display
                    os.environ["DISPLAY"] = display
                    return display

        except Exception as e:
            logger.warning(f"VNC 自动启动异常: {e}")

    except ImportError:
        logger.debug("VNC 管理器不可用，跳过 VNC 集成")
    except Exception as e:
        logger.warning(f"VNC 检测异常: {e}")

    # 2. VNC 不可用 → 独立启动 Xvfb
    return _start_xvfb()


# ── MCP 通信常量 ──────────────────────────────────────────────

# chrome-devtools-mcp 支持的完整工具列表 (v0.21.0)
# 参见: https://github.com/ChromeDevTools/chrome-devtools-mcp
MCP_TOOLS = {
    # 导航 (Navigation)
    "navigate_page": {"description": "导航到指定 URL"},
    "close_page": {"description": "关闭指定页面"},
    "list_pages": {"description": "列出所有打开的页面"},
    "select_page": {"description": "切换到指定页面"},
    "new_page": {"description": "打开新标签页"},
    "wait_for": {"description": "等待指定条件"},
    # 输入自动化 (Input Automation)
    "click": {"description": "点击页面元素"},
    "drag": {"description": "拖拽元素"},
    "fill": {"description": "填写输入框"},
    "fill_form": {"description": "填写表单"},
    "handle_dialog": {"description": "处理弹窗对话框"},
    "hover": {"description": "悬停在元素上"},
    "press_key": {"description": "按下键盘按键"},
    "type_text": {"description": "输入文本"},
    "upload_file": {"description": "上传文件"},
    # 调试 (Debugging)
    "evaluate_script": {"description": "执行 JavaScript"},
    "take_screenshot": {"description": "截取页面截图"},
    "take_snapshot": {"description": "获取页面可访问性树快照"},
    "list_console_messages": {"description": "列出控制台消息"},
    "get_console_message": {"description": "获取控制台消息详情"},
    "lighthouse_audit": {"description": "运行 Lighthouse 审计"},
    # 性能 (Performance)
    "performance_start_trace": {"description": "开始性能追踪"},
    "performance_stop_trace": {"description": "停止性能追踪"},
    "performance_analyze_insight": {"description": "分析性能洞察"},
    "take_memory_snapshot": {"description": "获取内存快照"},
    # 网络 (Network)
    "list_network_requests": {"description": "列出网络请求"},
    "get_network_request": {"description": "获取网络请求详情"},
    # 模拟 (Emulation)
    "emulate": {"description": "设备模拟(视口、深色模式、地理位置等)"},
    "resize_page": {"description": "调整页面大小"},
}

# MCP 协议常量
_MCP_JSONRPC_VERSION = "2024-11-05"
_MCP_INITIALIZE_TIMEOUT = 15  # 秒


class MCPClient:
    """
    本地 MCP 客户端 — 通过 stdio 与 chrome-devtools-mcp 通信。

    使用方法:
        client = MCPClient()
        await client.start()
        result = await client.call_tool("navigate_page", {"url": "https://example.com"})
        await client.stop()
    """

    def __init__(self, headless: bool = True, slim: bool = False):
        self._process: Optional[subprocess.Popen] = None
        self._headless = headless
        self._slim = slim
        self._request_id = 0
        self._response_queue: asyncio.Queue = asyncio.Queue()
        self._notification_queue: asyncio.Queue = asyncio.Queue()
        self._initialized = False
        self._server_capabilities: dict = {}
        self._lock = asyncio.Lock()
        # [v1.17.0] 有头模式 DISPLAY 覆盖
        self._display_override: Optional[str] = None
        # [v1.19.0] stderr 日志收集（用于调试 MCP Server 启动失败）
        self._stderr_buffer: List[str] = []

    async def start(self) -> bool:
        """启动 MCP Server 子进程"""
        # 检查 npx 是否可用
        if not shutil.which("npx"):
            if not shutil.which("node"):
                return False
            # 尝试直接用 node 运行
            logger.error("npx 不可用，请安装 Node.js (>= 20.19)")
            return False

        args = ["npx", "-y", "chrome-devtools-mcp@0.21.0"]
        if self._headless:
            args.append("--headless")
        if self._slim:
            args.append("--slim")

        # ── 环境检测 ──
        # 桌面环境 (Windows/Mac/Linux with real DISPLAY) 直接用系统 Chrome
        # 容器/Termux 环境才需要容器参数和自动安装 Chromium
        try:
            from core.env_detect import is_desktop, needs_container_args
            _is_desktop_env = is_desktop()
            _need_container_args = needs_container_args()
        except ImportError:
            _is_desktop_env = _has_display()  # 降级: 有 DISPLAY 视为桌面
            _need_container_args = not _has_display()

        # [v1.16.4] 自动检测可用的浏览器，通过 --executablePath 传递给 MCP
        browser_path = self._detect_browser()
        env = {**os.environ, "NO_COLOR": "1"}
        if browser_path:
            args.extend(["--executablePath", browser_path])
            logger.info(f"使用浏览器: {browser_path}")
        else:
            # 桌面环境不应自动安装 Chromium（用户应自行安装 Chrome）
            # 容器/Termux 环境才自动安装
            if not _is_desktop_env:
                # [v1.16.7] 自动安装 Chromium
                installed = self._try_install_chromium()
                if installed:
                    browser_path = self._detect_browser()
                    if browser_path:
                        args.extend(["--executablePath", browser_path])
                        logger.info(f"已自动安装 Chromium: {browser_path}")
            if not browser_path:
                if _is_desktop_env:
                    logger.error(
                        "未找到 Chrome/Chromium 浏览器。"
                        "请安装 Google Chrome: https://www.google.com/chrome/"
                    )
                else:
                    logger.error(
                        "未找到 Chrome/Chromium 浏览器且自动安装失败。"
                        "请手动安装: apt install -y chromium-browser "
                        "或 apt install -y chromium"
                    )

        # 容器/Termux 环境需要 --no-sandbox，桌面环境不需要
        if _need_container_args:
            args.extend(["--chromeArg", "--no-sandbox", "--chromeArg", "--disable-setuid-sandbox"])
            logger.info("容器/Termux 环境，已添加 --no-sandbox 等容器参数")
        else:
            logger.info("桌面环境，使用系统 Chrome 原生参数")

        # [v1.17.0] 有头模式: 设置 DISPLAY 环境变量
        # 优先级: 用户指定 DISPLAY > VNC 远程桌面 > 独立 Xvfb > 降级 headless
        if not self._headless:
            # 优先使用用户指定的 DISPLAY
            if self._display_override:
                env["DISPLAY"] = self._display_override
                logger.info(f"有头模式: DISPLAY={self._display_override}")
            # 无可用显示时，优先尝试 VNC 远程桌面，再尝试独立 Xvfb
            elif not _has_display():
                display = _ensure_display()
                if display:
                    env["DISPLAY"] = display
                    logger.info(f"有头模式: DISPLAY={display}（通过 VNC/Xvfb 提供）")
                else:
                    # 都不可用，降级为 headless 模式
                    self._headless = True
                    args.append("--headless")
                    logger.warning("无 DISPLAY 环境且 VNC/Xvfb 均不可用，自动降级为 headless 模式")

        logger.info(f"启动 chrome-devtools-mcp: {' '.join(args)}")

        try:
            # 启动 MCP Server 子进程
            # [v1.19.0] stderr 使用 PIPE 并在后台线程中持续读取，避免管道缓冲区满导致子进程阻塞
            self._stderr_buffer = []
            self._process = subprocess.Popen(
                args,
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                env=env,
            )

            # 启动读取线程（stdout 和 stderr 各一个，防止管道阻塞）
            loop = asyncio.get_event_loop()
            loop.run_in_executor(None, self._read_stdout)
            loop.run_in_executor(None, self._read_stderr)

            # 执行 MCP 握手
            success = await self._handshake()
            if success:
                self._initialized = True
                logger.info("chrome-devtools-mcp 已连接并初始化")
            else:
                # 握手失败时，收集 stderr 输出用于诊断
                stderr_output = self._get_stderr_tail()
                if stderr_output:
                    logger.error(f"MCP 握手失败，stderr 输出:\n{stderr_output}")
                else:
                    logger.error(
                        "MCP 握手失败，无 stderr 输出。可能原因: "
                        "1) Node.js 版本过低 (需要 >= 20.19); "
                        "2) chrome-devtools-mcp@0.21.0 下载失败; "
                        "3) Chrome/Chromium 未找到且自动安装失败"
                    )
            return success

        except FileNotFoundError:
            logger.error("无法启动 chrome-devtools-mcp: npx 命令未找到。请安装 Node.js >= 20.19: https://nodejs.org/")
            return False
        except Exception as e:
            stderr_output = self._get_stderr_tail()
            logger.error(f"启动 chrome-devtools-mcp 失败: {e}")
            if stderr_output:
                logger.error(f"MCP Server stderr:\n{stderr_output}")
            self._cleanup()
            return False

    @staticmethod
    def _detect_browser() -> Optional[str]:
        """[v1.16.2] 自动检测可用的浏览器（Chrome/Chromium/Brave 等）。

        检测顺序:
          1. 环境变量 CHROME_PATH / BROWSER_PATH
          2. PATH 中的常见浏览器命令
          3. Linux 常见安装路径
          4. macOS 特定路径
          5. Puppeteer 缓存目录
        """
        # 1. 环境变量
        for env_key in ("CHROME_PATH", "BROWSER_PATH"):
            env_val = os.environ.get(env_key, "").strip()
            if env_val and os.path.isfile(env_val):
                return env_val

        # 2. PATH 中的浏览器命令
        path_candidates = [
            "google-chrome", "google-chrome-stable", "google-chrome-beta",
            "chromium-browser", "chromium",
            "brave-browser", "microsoft-edge",
        ]
        for cmd in path_candidates:
            found = shutil.which(cmd)
            if found:
                return found

        # 3. Linux 常见安装路径
        linux_paths = [
            "/usr/bin/google-chrome",
            "/usr/bin/google-chrome-stable",
            "/usr/bin/chromium-browser",
            "/usr/bin/chromium",
            "/snap/bin/chromium",
            "/usr/bin/brave-browser",
            "/usr/bin/microsoft-edge",
            "/opt/google/chrome/chrome",
            "/opt/microsoft/msedge/msedge",
        ]
        for p in linux_paths:
            if os.path.isfile(p) and os.access(p, os.X_OK):
                return p

        # 4. macOS 特定路径
        macos_paths = [
            "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
            "/Applications/Chromium.app/Contents/MacOS/Chromium",
            "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
            "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
        ]
        for p in macos_paths:
            if os.path.isfile(p) and os.access(p, os.X_OK):
                return p

        # 5. Puppeteer 缓存目录
        home = os.path.expanduser("~")
        puppeteer_cache_dirs = [
            os.path.join(home, ".cache", "puppeteer", "chrome"),
            os.path.join(home, ".cache", "puppeteer"),
            os.path.join(home, ".cache", "ms-playwright"),
        ]
        for cache_dir in puppeteer_cache_dirs:
            if os.path.isdir(cache_dir):
                # 查找可执行文件
                for root, dirs, files in os.walk(cache_dir):
                    for file in files:
                        if file in ("chrome", "chromium-browser"):
                            chrome_path = os.path.join(root, file)
                            if os.path.isfile(chrome_path) and os.access(chrome_path, os.X_OK):
                                return chrome_path

        return None

    def _try_install_chromium() -> bool:
        """[v1.16.8→18] 尝试自动安装 Chrome/Chromium。

        安装策略（按优先级）:
          0. 检查并安装 Chrome 运行所需的共享库 (libnss3, libatk, etc.)
          1. npx puppeteer browsers install chrome@stable — 下载官方 Chrome，最可靠
          2. npx puppeteer install — 旧版命令格式
          3. apt install chromium-browser — 系统包
          4. apt install chromium — 备用包名

        容器环境缺少 libnss3/libatk 等共享库时，apt 安装的 Chromium 会崩溃，
        而 puppeteer 下载的 Chrome 是自包含的，不受影响。
        """
        # [v1.16.18] 先尝试安装 Chrome 运行所需的共享库
        import os as _os
        _need_sudo = _os.getuid() != 0 if hasattr(_os, 'getuid') else False
        _sudo = ["sudo"] if _need_sudo else []
        if shutil.which("apt"):
            try:
                logger.info("安装 Chrome 运行所需的共享库...")
                libs = [
                    "libnss3", "libatk1.0-0", "libatk-bridge2.0-0",
                    "libcups2", "libdrm2", "libxkbcommon0",
                    "libxcomposite1", "libxdamage1", "libxfixes3", "libxrandr2",
                    "libgbm1", "libpango-1.0-0", "libcairo2",
                    "libasound2", "libatspi2.0-0",
                ]
                result = subprocess.run(
                    _sudo + ["apt-get", "install", "-y", "--no-install-recommends"] + libs,
                    capture_output=True, text=True, timeout=120,
                )
                if result.returncode == 0:
                    logger.info("Chrome 共享库安装成功")
                else:
                    logger.warning(f"Chrome 共享库安装失败: {result.stderr[-200:] if result.stderr else ''}")
            except Exception as e:
                logger.warning(f"安装 Chrome 共享库异常: {e}")

        # 方法1: puppeteer install chrome（推荐，容器环境最可靠）
        if shutil.which("npx"):
            try:
                logger.info("正在通过 puppeteer 下载 Chrome...")
                result = subprocess.run(
                    ["npx", "-y", "puppeteer", "browsers", "install", "chrome@stable"],
                    capture_output=True, text=True, timeout=300,
                )
                if result.returncode == 0:
                    logger.info("puppeteer install chrome 成功")
                    return True
                # 也尝试旧版命令格式
                result2 = subprocess.run(
                    ["npx", "-y", "puppeteer", "install"],
                    capture_output=True, text=True, timeout=300,
                )
                if result2.returncode == 0:
                    logger.info("puppeteer install 成功")
                    return True
                logger.warning(
                    f"puppeteer install 失败: "
                    f"{result.stderr[-300:] if result.stderr else result.stdout[-300:]}"
                )
            except subprocess.TimeoutExpired:
                logger.warning("puppeteer install 超时 (5分钟)")
            except Exception as e:
                logger.warning(f"puppeteer install 异常: {e}")

        # 方法2: apt install（后备）
        if shutil.which("apt"):
            packages = ["chromium-browser", "chromium"]
            for pkg in packages:
                try:
                    logger.info(f"正在 apt install {pkg}...")
                    result = subprocess.run(
                        _sudo + ["apt", "install", "-y", pkg],
                        capture_output=True, text=True, timeout=120,
                    )
                    if result.returncode == 0:
                        logger.info(f"成功安装 {pkg}")
                        return True
                    else:
                        logger.warning(
                            f"apt install {pkg} 失败: "
                            f"{result.stderr[-300:] if result.stderr else result.stdout[-300:]}"
                        )
                except Exception as e:
                    logger.warning(f"apt install {pkg} 异常: {e}")

        return False

    def _read_stdout(self):
        """持续读取 MCP Server 的 stdout"""
        if not self._process or not self._process.stdout:
            return
        try:
            for line in self._process.stdout:
                line = line.decode("utf-8", errors="replace").strip()
                if not line:
                    continue
                try:
                    msg = json.loads(line)
                    # 通过 id 区分是请求/通知/响应
                    msg_id = msg.get("id")
                    if msg_id is not None:
                        self._response_queue.put_nowait(msg)
                    else:
                        self._notification_queue.put_nowait(msg)
                except json.JSONDecodeError:
                    logger.debug(f"非 JSON 输出: {line[:200]}")
                except Exception:
                    pass
        except Exception:
            pass
        finally:
            # [v1.18.3] stdout EOF = MCP Server 或 Chrome 已断开
            # 标记为未初始化，下次 call_tool 会自动重建
            self._initialized = False
            logger.warning("MCP stdout 读取结束（Server 或 Chrome 已断开）")

    def _read_stderr(self):
        """[v1.19.0] 持续读取 MCP Server 的 stderr 并记录到日志。

        必须在后台线程中持续读取 stderr，否则管道缓冲区满后
        子进程会阻塞，导致 JSON-RPC 通信超时。
        stderr 输出会同时收集到 _stderr_buffer 中，用于启动失败时的诊断。
        """
        if not self._process or not self._process.stderr:
            return
        try:
            for raw_line in self._process.stderr:
                line_text = raw_line.decode("utf-8", errors="replace").rstrip("\n")
                if line_text:
                    self._stderr_buffer.append(line_text)
                    # 只保留最近的 200 行，防止内存无限增长
                    if len(self._stderr_buffer) > 200:
                        self._stderr_buffer = self._stderr_buffer[-100:]
                    logger.debug(f"MCP stderr: {line_text}")
        except Exception:
            pass

    def _get_stderr_tail(self, max_lines: int = 30) -> str:
        """[v1.19.0] 获取最近的 stderr 输出（用于错误诊断）"""
        if not self._stderr_buffer:
            return ""
        lines = self._stderr_buffer[-max_lines:]
        return "\n".join(lines)

    async def _handshake(self) -> bool:
        """执行 MCP 初始化握手"""
        try:
            # 1. 发送 initialize 请求
            response = await self._send_request("initialize", {
                "protocolVersion": _MCP_JSONRPC_VERSION,
                "capabilities": {},
                "clientInfo": {"name": "myagent-ai", "version": "1.12.5"},
            }, timeout=_MCP_INITIALIZE_TIMEOUT)

            if not response or "result" not in response:
                logger.error("MCP initialize 失败: 无有效响应")
                return False

            result = response["result"]
            self._server_capabilities = result.get("capabilities", {})
            logger.info(f"MCP Server 已初始化, capabilities: {list(self._server_capabilities.keys())}")

            # 2. 发送 initialized 通知
            await self._send_notification("notifications/initialized", {})
            return True

        except asyncio.TimeoutError:
            logger.error("MCP initialize 超时")
            return False
        except Exception as e:
            logger.error(f"MCP 握手失败: {e}")
            return False

    async def _send_request(self, method: str, params: dict = None, timeout: float = 60) -> Optional[dict]:
        """发送 JSON-RPC 请求并等待响应"""
        async with self._lock:
            self._request_id += 1
            request_id = self._request_id

        message = {
            "jsonrpc": "2.0",
            "id": request_id,
            "method": method,
        }
        if params:
            message["params"] = params

        try:
            self._write_message(message)

            # 等待匹配的响应
            deadline = time.time() + timeout
            while time.time() < deadline:
                remaining = deadline - time.time()
                if remaining <= 0:
                    break
                try:
                    response = await asyncio.wait_for(
                        self._response_queue.get(), timeout=min(remaining, 2)
                    )
                    if response.get("id") == request_id:
                        if "error" in response:
                            err = response["error"]
                            logger.error(f"MCP 错误 [{method}]: {err.get('message', err)}")
                            return {"error": err}
                        return response
                    else:
                        # 不匹配的响应，放回队列
                        self._response_queue.put_nowait(response)
                except asyncio.TimeoutError:
                    continue

            logger.error(f"MCP 请求超时 [{method}] ({timeout}s)")
            return {"error": {"message": f"请求超时 ({timeout}s)"}}

        except Exception as e:
            logger.error(f"MCP 请求异常 [{method}]: {e}")
            return {"error": {"message": str(e)}}

    async def _send_notification(self, method: str, params: dict = None):
        """发送 JSON-RPC 通知（无需响应）"""
        message = {
            "jsonrpc": "2.0",
            "method": method,
        }
        if params:
            message["params"] = params
        self._write_message(message)

    def _write_message(self, message: dict):
        """写入一行 JSON 到 stdin"""
        if not self._process or not self._process.stdin:
            raise RuntimeError("MCP Server 子进程未运行")
        data = json.dumps(message, ensure_ascii=False)
        self._process.stdin.write((data + "\n").encode("utf-8"))
        self._process.stdin.flush()

    async def call_tool(self, tool_name: str, arguments: dict = None, timeout: float = 120) -> dict:
        """
        调用 MCP 工具。

        Args:
            tool_name: 工具名称 (如 "navigate_page", "click")
            arguments: 工具参数
            timeout: 超时时间(秒)

        Returns:
            MCP 工具调用的 result 字段内容
        """
        if not self._initialized:
            if not await self.start():
                return {"error": "MCP Server 未初始化"}

        params = arguments or {}
        logger.info(f"MCP 调用工具: {tool_name} (参数: {list(params.keys())})")

        response = await self._send_request("tools/call", {
            "name": tool_name,
            "arguments": params,
        }, timeout=timeout)

        if not response:
            return {"error": "MCP 无响应"}

        if "error" in response:
            err = response["error"]
            return {"error": err.get("message", str(err))}

        result = response.get("result", {})
        # [v1.16.18] 检查 MCP isError 标志 — 之前未映射到 error 字段，导致 skill 忽略错误
        is_error = result.get("isError", False)
        content = result.get("content", [])
        if content and isinstance(content, list):
            # 合并所有文本内容
            texts = []
            images = []
            for item in content:
                if item.get("type") == "text":
                    texts.append(item.get("text", ""))
                elif item.get("type") == "image":
                    images.append(item.get("data", ""))
            combined_text = "\n".join(texts) if texts else ""
            ret = {
                "text": combined_text,
                "images": images,
                "isError": is_error,
            }
            # [v1.16.18] isError 时同时设置 error 字段，让所有 skill 的 error 检查都能捕获
            if is_error:
                ret["error"] = combined_text or "MCP 工具执行失败"
            return ret

        return result

    async def list_tools(self) -> List[dict]:
        """列出 MCP Server 支持的所有工具"""
        if not self._initialized:
            if not await self.start():
                return []

        response = await self._send_request("tools/list", {}, timeout=10)
        if not response or "result" not in response:
            return []

        tools = response["result"].get("tools", [])
        return tools

    async def stop(self):
        """停止 MCP Server"""
        self._cleanup()

    def _cleanup(self):
        """清理子进程（包括 Chrome/Chromium 浏览器）

        [v1.18.3] 修复 'Target closed' 问题：
        终止 MCP Server 后，额外杀死残留的 Chrome 进程，
        避免下次启动时因锁文件冲突导致 'Target closed'。
        """
        self._initialized = False
        if self._process:
            try:
                self._process.stdin.close()
            except Exception:
                pass
            try:
                self._process.terminate()
                self._process.wait(timeout=5)
            except Exception:
                try:
                    self._process.kill()
                except Exception:
                    pass
            self._process = None

        # [v1.18.3] 清理残留的 Chrome/Chromium 进程
        # chrome-devtools-mcp 启动的 Chrome 可能不会随 MCP 进程一起退出
        self._kill_stale_chrome()

        # 清理 Xvfb 虚拟显示
        _stop_xvfb()

    def _kill_stale_chrome(self):
        """清理残留的 Chrome/Chromium 进程

        查找并杀死由 chrome-devtools-mcp 启动的 Chrome 进程。
        这些进程可能导致下次启动时 'Target closed' 错误。
        
        [v1.18.5] 扩展匹配模式，覆盖 chrome/chromium/chromium-browser/headless_shell
        """
        import signal as _sig
        try:
            # [v1.18.5] 更宽泛的匹配模式，覆盖各种 Chrome 变体
            # 匹配所有带 --remote-debugging-port 的 Chrome 相关进程
            patterns = [
                "chromium-browser.*--remote-debugging-port",
                "chromium.*--remote-debugging-port",
                "chrome.*--remote-debugging-port",
                "google-chrome.*--remote-debugging-port",
                "headless_shell.*--remote-debugging-port",
            ]
            all_pids = set()
            for pattern in patterns:
                try:
                    result = subprocess.run(
                        ["pgrep", "-f", pattern],
                        capture_output=True, text=True, timeout=5,
                    )
                    if result.returncode == 0 and result.stdout.strip():
                        for pid_str in result.stdout.strip().split("\n"):
                            # 排除 pgrep 自身和 npx/node 进程
                            try:
                                pid = int(pid_str.strip())
                                if pid != os.getpid():
                                    all_pids.add(pid)
                            except ValueError:
                                pass
                except Exception:
                    pass

            if not all_pids:
                return

            logger.info(f"清理 {len(all_pids)} 个残留 Chrome 进程: {all_pids}")
            for pid in all_pids:
                try:
                    os.kill(pid, _sig.SIGTERM)
                    logger.debug(f"发送 SIGTERM to Chrome PID={pid}")
                except (ProcessLookupError, PermissionError, ValueError):
                    pass

            # 等一小会让 Chrome 优雅退出
            import time
            time.sleep(1.0)

            # 还没退就强杀
            for pid in all_pids:
                try:
                    os.kill(pid, _sig.SIGKILL)
                except (ProcessLookupError, PermissionError, ValueError):
                    pass

            # 额外等待，确保端口释放
            time.sleep(0.5)
        except Exception as e:
            logger.debug(f"清理 Chrome 进程异常: {e}")

    def is_running(self) -> bool:
        """检查 MCP Server 是否正在运行"""
        return self._process is not None and self._process.poll() is None

    @staticmethod
    def diagnose() -> Dict[str, Any]:
        """[v1.19.0] 检查浏览器自动化环境是否就绪，返回诊断信息。

        用于在浏览器启动失败时提供详细的错误原因。
        检查项:
          1. Node.js 是否安装及版本
          2. npx 是否可用
          3. Chrome/Chromium 是否找到
          4. chrome-devtools-mcp npm 包是否可访问

        Returns:
            dict with keys: ok (bool), checks (list), summary (str)
        """
        results: Dict[str, Any] = {"ok": True, "checks": [], "summary": ""}

        # 1. 检查 Node.js
        node_path = shutil.which("node")
        if not node_path:
            results["ok"] = False
            results["checks"].append({
                "name": "Node.js", "status": "missing",
                "message": "Node.js 未安装。需要 >= 20.19。请安装: https://nodejs.org/",
            })
        else:
            try:
                ver_result = subprocess.run(
                    ["node", "--version"], capture_output=True, text=True, timeout=5,
                )
                version = ver_result.stdout.strip()
                results["checks"].append({
                    "name": "Node.js", "status": "ok",
                    "message": f"Node.js {version} ({node_path})",
                })
            except Exception as e:
                results["checks"].append({
                    "name": "Node.js", "status": "error", "message": str(e),
                })

        # 2. 检查 npx
        npx_path = shutil.which("npx")
        if not npx_path:
            results["ok"] = False
            results["checks"].append({
                "name": "npx", "status": "missing",
                "message": "npx 未安装（通常随 Node.js 一起安装）",
            })
        else:
            results["checks"].append({
                "name": "npx", "status": "ok",
                "message": f"npx 可用 ({npx_path})",
            })

        # 3. 检查 Chrome/Chromium
        browser_path = MCPClient._detect_browser()
        if browser_path:
            results["checks"].append({
                "name": "Chrome/Chromium", "status": "ok",
                "message": f"浏览器已找到: {browser_path}",
            })
        else:
            results["ok"] = False
            results["checks"].append({
                "name": "Chrome/Chromium", "status": "missing",
                "message": (
                    "未找到 Chrome/Chromium。系统会尝试自动安装，"
                    "或手动安装: apt install -y chromium-browser 或 apt install -y chromium"
                ),
            })

        # 4. 检查 npm / chrome-devtools-mcp 包可访问性
        npm_path = shutil.which("npm")
        if not npm_path:
            results["checks"].append({
                "name": "chrome-devtools-mcp (npm)", "status": "error",
                "message": "npm 未安装，无法验证 chrome-devtools-mcp 包",
            })
        else:
            try:
                pkg_result = subprocess.run(
                    ["npm", "view", "chrome-devtools-mcp", "version"],
                    capture_output=True, text=True, timeout=30,
                )
                if pkg_result.returncode == 0 and pkg_result.stdout.strip():
                    ver = pkg_result.stdout.strip()
                    results["checks"].append({
                        "name": "chrome-devtools-mcp (npm)", "status": "ok",
                        "message": f"npm 注册表中可用，最新版本: {ver} (当前固定: 0.21.0)",
                    })
                else:
                    results["checks"].append({
                        "name": "chrome-devtools-mcp (npm)", "status": "warning",
                        "message": (
                            f"无法查询 npm 注册表: "
                            f"{pkg_result.stderr[:200] if pkg_result.stderr else 'unknown error'}"
                        ),
                    })
            except Exception as e:
                results["checks"].append({
                    "name": "chrome-devtools-mcp (npm)", "status": "error",
                    "message": str(e),
                })

        # 构建摘要
        failed = [c for c in results["checks"] if c["status"] in ("missing", "error")]
        if not failed:
            results["summary"] = "浏览器自动化环境就绪"
        else:
            results["summary"] = "环境检查发现问题: " + "; ".join(
                f"{c['name']}({c['status']})" for c in failed
            )

        return results


# ── 全局 MCP 客户端单例 ──────────────────────────────────────

_mcp_client: Optional[MCPClient] = None
_mcp_client_headed: Optional[MCPClient] = None  # 有头模式单例
_mcp_lock = asyncio.Lock()


def _is_vnc_running() -> bool:
    """检查 VNC 远程桌面是否正在运行"""
    try:
        from core.vnc_manager import get_vnc_manager
        mgr = get_vnc_manager()
        return mgr.is_running
    except Exception:
        return False


def _get_vnc_display() -> Optional[str]:
    """获取 VNC 虚拟显示号"""
    try:
        from core.vnc_manager import get_vnc_manager
        mgr = get_vnc_manager()
        if mgr.is_running:
            return mgr.display
    except Exception:
        pass
    return None


async def get_mcp_client(headless: Optional[bool] = None) -> MCPClient:
    """获取全局 MCP 客户端（单例）

    Args:
        headless: None=自动检测（VNC运行时有头模式，否则无头）,
                  True=强制无头, False=强制有头

    [v1.16.18] 增强崩溃恢复：
    - 检测到旧实例崩溃时，先清理再重建
    - 重建后验证连接，如果仍然失败则彻底重置
    
    [v1.17.0] 支持有头模式：
    - VNC 远程桌面运行时，自动使用有头浏览器
    - 有头模式设置 DISPLAY 环境变量指向 Xvfb 虚拟显示
    - 可通过远程桌面看到浏览器操作界面
    """
    global _mcp_client, _mcp_client_headed

    # 决定使用有头还是无头模式
    if headless is None:
        headless = not _is_vnc_running()

    # 选择对应的单例和锁
    target_headed = not headless
    client_ref = "_mcp_client_headed" if target_headed else "_mcp_client"

    async with _mcp_lock:
        # 获取或创建客户端
        client = globals()[client_ref]

        if client is None:
            client = MCPClient(headless=not target_headed)

            # 有头模式：设置 DISPLAY 环境变量
            if target_headed:
                display = _get_vnc_display()
                if display:
                    logger.info(f"有头模式: 使用 DISPLAY={display}")
                    # 在 start() 中会自动应用 DISPLAY
                    client._display_override = display
                else:
                    logger.warning("有头模式请求但 VNC 未运行，回退到无头模式")
                    client = MCPClient(headless=True)
                    target_headed = False
                    client_ref = "_mcp_client"

            ok = await client.start()
            if ok:
                globals()[client_ref] = client
            else:
                logger.error(f"MCP 客户端启动失败 (headless={not target_headed})")
                globals()[client_ref] = None
                # 如果有头模式失败，回退到无头
                if target_headed:
                    logger.info("有头模式启动失败，回退到无头模式")
                    if _mcp_client is None:
                        _mcp_client = MCPClient(headless=True)
                        ok2 = await _mcp_client.start()
                        if not ok2:
                            _mcp_client = None
                    return _mcp_client
                return None

            return client

        # 检查实例是否仍然健康
        if not client.is_running():
            logger.warning("MCP 客户端进程已退出，正在清理并重建...")
            try:
                client._cleanup()
            except Exception:
                pass
            globals()[client_ref] = None
            new_client = MCPClient(headless=not target_headed)
            if target_headed:
                display = _get_vnc_display()
                if display:
                    new_client._display_override = display
            ok = await new_client.start()
            if ok:
                globals()[client_ref] = new_client
            else:
                logger.error("MCP 客户端重建失败")
                globals()[client_ref] = None
            return globals()[client_ref]

        # 进程在运行，但可能初始化状态丢失
        if not client._initialized:
            logger.warning("MCP 客户端未初始化，尝试重新启动...")
            try:
                client._cleanup()
            except Exception:
                pass
            globals()[client_ref] = None
            new_client = MCPClient(headless=not target_headed)
            if target_headed:
                display = _get_vnc_display()
                if display:
                    new_client._display_override = display
            ok = await new_client.start()
            if ok:
                globals()[client_ref] = new_client
            else:
                logger.error("MCP 客户端重新启动失败")
                globals()[client_ref] = None
            return globals()[client_ref]

        return client


async def rebuild_mcp_client(force_headless: bool = False) -> None:
    """强制重建 MCP 客户端（用于 VNC 启动/停止后切换模式）

    Args:
        force_headless: 强制使用无头模式
    """
    global _mcp_client, _mcp_client_headed

    async with _mcp_lock:
        # 清理有头客户端
        if _mcp_client_headed:
            try:
                _mcp_client_headed._cleanup()
            except Exception:
                pass
            _mcp_client_headed = None

        if force_headless and _mcp_client:
            # 也清理无头客户端（强制重建）
            try:
                _mcp_client._cleanup()
            except Exception:
                pass
            _mcp_client = None


def _ensure_node_deps() -> Optional[str]:
    """检查 Node.js 依赖是否就绪，返回错误信息或 None"""
    if not shutil.which("node"):
        return "需要 Node.js (>= 20.19)。请安装: https://nodejs.org/"
    if not shutil.which("npx"):
        return "需要 npx (Node.js 自带)。请重新安装 Node.js"
    return None


# [v1.18.9] 共享的 MCP 工具调用函数，带自动重试（所有浏览器技能通用）
_MCP_CRASH_KEYWORDS = [
    "Target closed", "Target destroyed", "Protocol error",
    "Browser closed", "Connection closed", "Session closed",
    "Connection refused", "net::ERR_CONNECTION_REFUSED",
]

async def _call_mcp_with_retry(
    tool_name: str,
    arguments: dict,
    timeout: float = 30,
    max_retries: int = 1,
) -> dict:
    """调用 MCP 工具，遇到 Chrome 崩溃时自动重建客户端并重试。

    Args:
        tool_name: MCP 工具名 (navigate_page, click, fill, etc.)
        arguments: 工具参数字典
        timeout: 单次调用超时秒数
        max_retries: 额外重试次数 (默认 1)

    Returns:
        MCP 工具返回的 dict
    """
    last_error = None
    for attempt in range(max_retries + 1):
        client = await get_mcp_client()
        if not client or not client.is_running() or not client._initialized:
            if attempt < max_retries:
                logger.warning("[MCP] 客户端不可用，正在重建...")
                try:
                    await rebuild_mcp_client()
                except Exception:
                    pass
                await asyncio.sleep(0.5)
                continue
            return {"error": "MCP 客户端不可用，请重试"}

        try:
            result = await client.call_tool(tool_name, arguments, timeout=timeout)
            # 检查返回值中是否有崩溃指示
            result_str = str(result.get("error", "")) if isinstance(result, dict) else str(result)
            if any(kw in result_str for kw in _MCP_CRASH_KEYWORDS):
                if attempt < max_retries:
                    logger.warning(f"[MCP] Chrome 连接丢失 ({result_str[:120]})，正在重建 MCP 客户端并重试 ({attempt+1}/{max_retries})...")
                    try:
                        await rebuild_mcp_client()
                    except Exception:
                        pass
                    await asyncio.sleep(1.0)
                    continue
            return result
        except Exception as e:
            err_str = str(e)
            last_error = err_str
            if any(kw in err_str for kw in _MCP_CRASH_KEYWORDS):
                if attempt < max_retries:
                    logger.warning(f"[MCP] Chrome 异常 ({err_str[:120]})，正在重建 MCP 客户端并重试 ({attempt+1}/{max_retries})...")
                    try:
                        await rebuild_mcp_client()
                    except Exception:
                        pass
                    await asyncio.sleep(1.0)
                    continue
            # 非崩溃类错误，直接抛出
            raise

    return {"error": f"MCP 调用失败: {last_error or '重试耗尽'}"}


def _parse_json_response(text: str) -> Any:
    """尝试从 MCP 工具返回的文本中解析 JSON"""
    if not text:
        return text
    text = text.strip()
    # 尝试直接解析
    try:
        return json.loads(text)
    except (json.JSONDecodeError, ValueError):
        pass
    # 尝试提取 JSON 块
    import re
    # 匹配 ```json ... ``` 代码块
    json_block = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL)
    if json_block:
        try:
            return json.loads(json_block.group(1))
        except (json.JSONDecodeError, ValueError):
            pass
    # 匹配 { ... } 或 [ ... ]
    for start_char, end_char in [("{", "}"), ("[", "]")]:
        start = text.find(start_char)
        if start >= 0:
            depth = 0
            for i in range(start, len(text)):
                if text[i] == start_char:
                    depth += 1
                elif text[i] == end_char:
                    depth -= 1
                if depth == 0:
                    try:
                        return json.loads(text[start:i + 1])
                    except (json.JSONDecodeError, ValueError):
                        break
    return text


# ══════════════════════════════════════════════════════════════
#  技能定义 — 与旧 browser_skill.py 保持相同的接口，内部改为 MCP 调用
# ══════════════════════════════════════════════════════════════


class BrowserOpenSkill(Skill):
    """
    打开网页 - 打开指定 URL 并返回结构化的页面信息。

    使用 ChromeDev MCP (chrome-devtools-mcp) 的 navigate_page + take_snapshot 工具。
    返回页面的标题、可访问性树快照等信息。
    """

    name = "browser_open"
    description = (
        "【仅用于需要浏览器交互的场景】打开网页并返回结构化页面信息（标题、文本、链接、表单等）。"
        "注意：单纯搜索信息请用 web_search，读取网页正文请用 web_read。"
        "仅在需要填写表单、点击按钮、截图、执行JavaScript等浏览器交互时才使用此工具。"
    )
    category = "browser"
    parameters = [
        SkillParameter("url", "string", "要打开的网页 URL（必须以 http:// 或 https:// 开头）", required=True),
        SkillParameter("wait", "integer", "页面加载后额外等待时间（毫秒），用于等待动态内容渲染", required=False, default=3000),
    ]

    async def execute(self, url: str = "", wait: int = 3000, **kwargs) -> SkillResult:
        """执行：通过 MCP 打开 URL

        [v1.16.18] 增加 Chrome 崩溃自动重试：
        - 检测到 "Target closed" 或 "Protocol error" 时，强制重建 MCP 客户端
        - 最多重试 1 次
        """
        if not url:
            return SkillResult(success=False, error="缺少必需参数: url")

        # [v1.47.20] VNC 模式下：Chromium 不可用时回退到 stealth_browser_navigate
        try:
            from core.vnc_manager import get_vnc_manager
            vnc_mgr = get_vnc_manager()
            if vnc_mgr.is_running:
                # 检查是否有可用的 Chromium
                has_chrome = bool(shutil.which("chromium-browser") or shutil.which("chromium")
                                 or shutil.which("google-chrome"))
                if not has_chrome:
                    # VNC 模式下无 Chromium，回退到 stealth_browser
                    return SkillResult(
                        success=False,
                        error="VNC 远程桌面模式下没有 Chromium 浏览器，无法使用 browser_open。"
                              "请改用 stealth_browser_start + stealth_browser_navigate 操作 Firefox。"
                              "Firefox 在 VNC 模式下已可用。",
                    )
        except ImportError:
            pass  # vnc_manager 不可用，跳过检测

        # 检查依赖
        dep_err = await asyncio.get_event_loop().run_in_executor(None, _ensure_node_deps)
        if dep_err:
            # [v1.19.0] 依赖缺失时也运行诊断，给出完整信息
            diag = await asyncio.get_event_loop().run_in_executor(None, MCPClient.diagnose)
            diag_lines = [f"  - {c['name']}: {c['message']}" for c in diag.get("checks", [])]
            return SkillResult(
                success=False,
                error=f"ChromeDev MCP 依赖缺失: {dep_err}\n环境诊断:\n" + "\n".join(diag_lines),
            )

        global _mcp_client
        max_attempts = 2  # 最多重试 1 次
        for attempt in range(max_attempts):
            try:
                # [v1.19.0] 添加超时保护，防止 get_mcp_client() 永久阻塞
                # 首次启动可能需要下载 chrome-devtools-mcp 和 Chrome，给 90 秒
                mcp_timeout = 90 if attempt == 0 else 60
                try:
                    client = await asyncio.wait_for(
                        get_mcp_client(), timeout=mcp_timeout,
                    )
                except asyncio.TimeoutError:
                    logger.error(f"获取 MCP 客户端超时 ({mcp_timeout}s)")
                    diag = await asyncio.get_event_loop().run_in_executor(None, MCPClient.diagnose)
                    diag_lines = [f"  - {c['name']}: {c['message']}" for c in diag.get("checks", [])]
                    return SkillResult(
                        success=False,
                        error=(
                            f"浏览器启动超时 ({mcp_timeout}s)。"
                            f"首次启动可能需要下载 chrome-devtools-mcp 和 Chrome，请稍后重试。\n"
                            f"环境诊断:\n" + "\n".join(diag_lines)
                        ),
                    )

                if not client or not client.is_running() or not client._initialized:
                    # [v1.19.0] 启动失败时运行完整诊断
                    diag = await asyncio.get_event_loop().run_in_executor(None, MCPClient.diagnose)
                    diag_lines = [f"  - {c['name']}: {c['message']}" for c in diag.get("checks", [])]
                    return SkillResult(
                        success=False,
                        error=(
                            "浏览器启动失败。系统未安装 Chrome/Chromium 或 MCP Server 初始化失败。\n"
                            "环境诊断:\n" + "\n".join(diag_lines) + "\n\n"
                            "建议: apt install -y chromium-browser 或 apt install -y chromium"
                        ),
                    )

                # 1. 导航到目标 URL
                nav_result = await client.call_tool("navigate_page", {"url": url}, timeout=30)
                if nav_result.get("error"):
                    err_text = str(nav_result["error"])
                    # [v1.16.7] 检测 Chrome not found 错误，给出清晰提示
                    if "Could not find" in err_text and "Chrome" in err_text:
                        return SkillResult(
                            success=False,
                            error=f"浏览器未安装: {err_text}。"
                                  f"请执行: apt install -y chromium-browser 或 apt install -y chromium，"
                                  f"安装后重试即可。"
                        )
                    # [v1.16.18→18.3] 检测 Chrome 崩溃，彻底清理后重建 MCP 客户端并重试
                    _crash_keywords = ["Target closed", "Target destroyed", "Protocol error",
                                      "Browser closed", "Connection closed", "Session closed"]
                    if any(kw in err_text for kw in _crash_keywords) and attempt < max_attempts - 1:
                        logger.warning(f"Chrome 连接丢失 ({err_text[:100]})，正在重建 MCP 客户端并重试...")
                        # [v1.18.3] 彻底清理所有 MCP 客户端（包括残留 Chrome 进程）
                        await rebuild_mcp_client()
                        continue  # 重试
                    return SkillResult(success=False, error=f"导航失败: {err_text}")

                # 2. 等待动态内容
                if wait > 0:
                    await asyncio.sleep(wait / 1000)

                # 3. 获取页面快照 (可访问性树，包含标题、文本、链接等结构化信息)
                snapshot_result = await client.call_tool("take_snapshot", {}, timeout=30)
                snapshot_text = snapshot_result.get("text", "")

                # 4. 也获取页面标题用于简短描述
                title = ""
                # 从快照中提取标题
                if snapshot_text:
                    # take_snapshot 返回的是可访问性树，第一行通常是标题
                    first_lines = snapshot_text.split("\n")
                    if first_lines:
                        title = first_lines[0].strip("# ").strip()

                # 尝试通过 JS 获取结构化信息
                eval_result = await client.call_tool("evaluate_script", {
                    "script": """() => {
                    const links = Array.from(document.querySelectorAll('a[href]'))
                        .filter(a => a.offsetParent !== null)
                        .slice(0, 30)
                        .map(a => ({ text: a.innerText.trim().substring(0, 100), href: a.href }))
                        .filter(l => l.text);
                    const forms = Array.from(document.querySelectorAll('form')).slice(0, 10).map(form => {
                        const inputs = Array.from(form.querySelectorAll('input, textarea, select'))
                            .map(el => ({ tag: el.tagName.toLowerCase(), type: el.type || '', name: el.name || '', placeholder: el.placeholder || '' }));
                        return { action: form.action || '', method: (form.method || 'GET').toUpperCase(), inputs: inputs.slice(0, 10) };
                    });
                    return { title: document.title || '', text: document.body.innerText.substring(0, 15000), links, forms };
                }"""
                }, timeout=15)

                page_info = {}
                if eval_result.get("text"):
                    parsed = _parse_json_response(eval_result["text"])
                    if isinstance(parsed, dict):
                        page_info = parsed

                page_title = page_info.get("title", title)
                text = page_info.get("text", "")
                links = page_info.get("links", [])
                forms = page_info.get("forms", [])

                return SkillResult(
                    success=True,
                    data={
                        "title": page_title,
                        "text": text,
                        "links": links,
                        "forms": forms,
                        "url": url,
                        "snapshot": snapshot_text[:5000],  # 附上可访问性树快照供 LLM 参考
                    },
                    message=f"已打开: {page_title} (文本 {len(text)} 字符, {len(links)} 个链接)",
                )
            except Exception as e:
                # [v1.16.18→18.3] 异常时也检测 Chrome 崩溃并彻底重建
                _err_str = str(e)
                _crash_keywords = ["Target closed", "Target destroyed", "Protocol error",
                                  "Browser closed", "Connection closed", "Session closed"]
                if any(kw in _err_str for kw in _crash_keywords) and attempt < max_attempts - 1:
                    logger.warning(f"Chrome 异常 ({_err_str[:100]})，正在重建 MCP 客户端并重试...")
                    await rebuild_mcp_client()
                    continue  # 重试
                logger.error(f"浏览器打开失败: {e}")
                return SkillResult(success=False, error=f"浏览器打开失败: {e}")


class BrowserClickSkill(Skill):
    """
    点击页面元素 - 通过引用(ref)定位并点击元素。

    ChromeDev MCP 使用可访问性树引用来定位元素（比 CSS 选择器更可靠）。
    也可通过 CSS 选择器定位。
    """

    name = "browser_click"
    description = (
        "在当前浏览器页面中点击元素。支持通过可访问性树引用（ref）或 CSS 选择器定位。"
        "点击后会等待页面响应。推荐使用 ref 定位，更稳定可靠。"
    )
    category = "browser"
    parameters = [
        SkillParameter("ref", "string", "元素的可访问性树引用（推荐，如 'button[aria-label=\"登录\"]'）", required=False, default=""),
        SkillParameter("selector", "string", "元素的 CSS 选择器（如 'button#submit', 'a.login-link'）", required=False, default=""),
        SkillParameter("text", "string", "元素的可见文本（如 '登录', '搜索'）", required=False, default=""),
        SkillParameter("wait_after", "integer", "点击后等待时间（毫秒）", required=False, default=1000),
    ]

    async def execute(
        self,
        ref: str = "",
        selector: str = "",
        text: str = "",
        wait_after: int = 1000,
        **kwargs,
    ) -> SkillResult:
        """执行：通过 MCP 点击元素"""
        if not ref and not selector and not text:
            return SkillResult(success=False, error="必须提供 ref、selector 或 text 参数来定位元素")

        try:
            client = await get_mcp_client()

            # 构建定位参数
            params = {}
            if ref:
                params["ref"] = ref
            elif selector:
                params["selector"] = selector
            elif text:
                params["ref"] = text  # MCP click 支持 ref 参数

            click_result = await _call_mcp_with_retry("click", params, timeout=30)
            if click_result.get("isError") or click_result.get("error"):
                error_msg = click_result.get("error") or click_result.get("text", "点击失败")
                return SkillResult(success=False, error=f"点击元素失败: {error_msg}")

            # 等待页面响应
            if wait_after > 0:
                await asyncio.sleep(wait_after / 1000)

            # 获取点击后页面状态
            eval_result = await _call_mcp_with_retry("evaluate_script", {
                "script": "() => ({ title: document.title, url: window.location.href, text: document.body.innerText.substring(0, 3000) })"
            }, timeout=10)

            page_state = {}
            if eval_result.get("text"):
                parsed = _parse_json_response(eval_result["text"])
                if isinstance(parsed, dict):
                    page_state = parsed

            loc_desc = f"ref '{ref}'" if ref else (f"选择器 '{selector}'" if selector else f"文本 '{text}'")

            return SkillResult(
                success=True,
                data=page_state,
                message=f"已点击 {loc_desc}，当前页面: {page_state.get('title', '未知')}",
            )
        except Exception as e:
            logger.error(f"点击元素失败: {e}")
            return SkillResult(success=False, error=f"点击元素失败: {e}")


class BrowserFillSkill(Skill):
    """
    填写输入框 - 通过引用或选择器定位输入框并填入内容。
    """

    name = "browser_fill"
    description = (
        "在当前浏览器页面的输入框中填写内容。支持通过可访问性树引用（ref）或 CSS 选择器定位。"
        "填写前会自动清空原有内容。"
    )
    category = "browser"
    parameters = [
        SkillParameter("ref", "string", "输入框的可访问性树引用（推荐）", required=False, default=""),
        SkillParameter("selector", "string", "输入框的 CSS 选择器", required=False, default=""),
        SkillParameter("value", "string", "要填写的值", required=True),
        SkillParameter("press_enter", "boolean", "填写后是否按回车键（默认 false）", required=False, default=False),
    ]

    async def execute(
        self,
        ref: str = "",
        selector: str = "",
        value: str = "",
        press_enter: bool = False,
        **kwargs,
    ) -> SkillResult:
        """执行：通过 MCP 填写输入框"""
        if not value:
            return SkillResult(success=False, error="缺少必需参数: value")
        if not ref and not selector:
            return SkillResult(success=False, error="必须提供 ref 或 selector 参数来定位输入框")

        try:
            client = await get_mcp_client()

            params = {"text": value}
            if ref:
                params["ref"] = ref
            elif selector:
                params["selector"] = selector

            fill_result = await _call_mcp_with_retry("fill", params, timeout=30)
            if fill_result.get("isError") or fill_result.get("error"):
                error_msg = fill_result.get("error") or fill_result.get("text", "填写失败")
                return SkillResult(success=False, error=f"填写输入框失败: {error_msg}")

            # 按回车（可选）
            if press_enter:
                await _call_mcp_with_retry("press_key", {"key": "Enter"}, timeout=10)
                await asyncio.sleep(1)

            loc_desc = f"ref '{ref}'" if ref else f"选择器 '{selector}'"
            return SkillResult(
                success=True,
                data={"locator": loc_desc, "value_length": len(value), "value_preview": value[:100]},
                message=f"已在 {loc_desc} 填写内容（{len(value)} 字符）",
            )
        except Exception as e:
            logger.error(f"填写输入框失败: {e}")
            return SkillResult(success=False, error=f"填写输入框失败: {e}")


class BrowserScreenshotSkill(Skill):
    """
    页面截图 - 截取当前浏览器页面的截图。

    使用 ChromeDev MCP 的 take_screenshot 工具。
    截图以 base64 格式返回，同时保存为 PNG 文件。
    """

    name = "browser_screenshot"
    description = (
        "截取当前浏览器页面的截图，保存为 PNG 文件并返回文件路径。"
        "可用于 VLM 视觉分析。支持全页面截图或仅截取可视区域。"
    )
    category = "browser"
    parameters = [
        SkillParameter("full_page", "boolean", "是否截取整个页面（包括滚动区域）", required=False, default=False),
        SkillParameter("selector", "string", "截取特定元素的截图（CSS 选择器）", required=False, default=""),
    ]

    async def execute(
        self,
        full_page: bool = False,
        selector: str = "",
        **kwargs,
    ) -> SkillResult:
        """执行：通过 MCP 截取页面截图"""
        try:
            client = await get_mcp_client()

            params = {}
            # take_screenshot 的参数格式
            if selector:
                params["selector"] = selector

            shot_result = await _call_mcp_with_retry("take_screenshot", params, timeout=30)
            if shot_result.get("isError") or shot_result.get("error"):
                error_msg = shot_result.get("error") or shot_result.get("text", "截图失败")
                return SkillResult(success=False, error=f"截图失败: {error_msg}")

            # 处理返回的图片数据
            screenshot_path = None
            images = shot_result.get("images", [])

            if images:
                # MCP 返回 base64 编码的图片
                import base64
                timestamp = time.strftime("%Y%m%d_%H%M%S")
                screenshot_path = f"/tmp/myagent_gui_screenshot_{timestamp}.png"
                img_data = base64.b64decode(images[0])
                with open(screenshot_path, "wb") as f:
                    f.write(img_data)
                file_size = len(img_data)
            else:
                return SkillResult(success=False, error="截图未返回图片数据")

            return SkillResult(
                success=True,
                data={
                    "screenshot_path": screenshot_path,
                    "file_size_bytes": file_size,
                    "full_page": full_page,
                },
                message=f"截图已保存: {screenshot_path} ({file_size} 字节)",
                files=[screenshot_path] if screenshot_path else [],
            )
        except Exception as e:
            logger.error(f"截图失败: {e}")
            return SkillResult(success=False, error=f"截图失败: {e}")


class BrowserEvalSkill(Skill):
    """
    执行 JavaScript - 在当前浏览器页面中执行自定义 JavaScript 代码。
    """

    name = "browser_eval"
    description = (
        "在当前浏览器页面中执行 JavaScript 代码并返回结果。"
        "代码应返回一个可序列化的值。"
    )
    category = "browser"
    dangerous = True
    parameters = [
        SkillParameter("code", "string", "要执行的 JavaScript 代码（应包含 return 语句）", required=True),
    ]

    async def execute(self, code: str = "", **kwargs) -> SkillResult:
        """执行：通过 MCP 执行 JavaScript"""
        if not code:
            return SkillResult(success=False, error="缺少必需参数: code")

        try:
            client = await get_mcp_client()

            eval_result = await _call_mcp_with_retry("evaluate_script", {"script": code}, timeout=30)
            if eval_result.get("isError") or eval_result.get("error"):
                error_msg = eval_result.get("error") or eval_result.get("text", "执行失败")
                return SkillResult(success=False, error=f"JavaScript 执行失败: {error_msg}")

            result_text = eval_result.get("text", "")
            result_data = _parse_json_response(result_text)

            return SkillResult(
                success=True,
                data={"result": result_data},
                message=f"JavaScript 执行成功",
            )
        except Exception as e:
            logger.error(f"JavaScript 执行失败: {e}")
            return SkillResult(success=False, error=f"JavaScript 执行失败: {e}")


class BrowserNavigateSkill(Skill):
    """
    浏览器导航 - 控制浏览器的前进、后退、新标签页等操作。
    """

    name = "browser_navigate"
    description = (
        "控制浏览器导航操作：前进、后退、打开新标签页、切换标签页、列出标签页。"
        "浏览器保持持久会话。"
    )
    category = "browser"
    parameters = [
        SkillParameter("action", "string", "导航操作类型", required=True,
                        enum=["back", "forward", "new_tab", "switch_tab", "list_tabs"]),
        SkillParameter("url", "string", "新标签页的 URL", required=False, default=""),
        SkillParameter("page_id", "string", "目标标签页 ID", required=False, default=""),
    ]

    async def execute(
        self,
        action: str = "",
        url: str = "",
        page_id: str = "",
        **kwargs,
    ) -> SkillResult:
        """执行：浏览器导航操作"""
        if not action:
            return SkillResult(success=False, error="缺少必需参数: action")

        try:
            client = await get_mcp_client()

            if action == "list_tabs":
                result = await _call_mcp_with_retry("list_pages", {}, timeout=15)
                text = result.get("text", "")
                return SkillResult(
                    success=True,
                    data={"raw": text},
                    message=f"标签页列表: {text[:500]}",
                )

            elif action == "new_tab":
                result = await _call_mcp_with_retry("new_page", {}, timeout=15)
                if result.get("error"):
                    return SkillResult(success=False, error=f"打开新标签页失败: {result['error']}")
                text = result.get("text", "")
                if url:
                    nav_result = await _call_mcp_with_retry("navigate_page", {"url": url}, timeout=30)
                    if nav_result.get("error"):
                        return SkillResult(success=False, error=f"导航失败: {nav_result['error']}")
                    text += f" → {url}"
                return SkillResult(
                    success=True,
                    data={"raw": text},
                    message=f"新标签页已打开{f'，已导航到 {url}' if url else ''}",
                )

            elif action == "switch_tab":
                if not page_id:
                    return SkillResult(success=False, error="switch_tab 需要指定 page_id 参数")
                result = await _call_mcp_with_retry("select_page", {"pageId": page_id}, timeout=15)
                if result.get("error"):
                    return SkillResult(success=False, error=f"切换标签页失败: {result['error']}")
                return SkillResult(
                    success=True,
                    data={"page_id": page_id},
                    message=f"已切换到标签页: {page_id}",
                )

            elif action == "close_tab":
                result = await _call_mcp_with_retry("close_page", {}, timeout=15)
                return SkillResult(
                    success=True,
                    data={},
                    message="标签页已关闭",
                )

            elif action == "back":
                # 通过 JS 实现后退
                result = await _call_mcp_with_retry("evaluate_script", {
                    "script": "() => { window.history.back(); return document.title; }"
                }, timeout=15)
                await asyncio.sleep(1)
                title = result.get("text", "") if not result.get("error") else ""
                return SkillResult(
                    success=True,
                    data={"title": title},
                    message=f"已后退到: {title}",
                )

            elif action == "forward":
                result = await _call_mcp_with_retry("evaluate_script", {
                    "script": "() => { window.history.forward(); return document.title; }"
                }, timeout=15)
                await asyncio.sleep(1)
                title = result.get("text", "") if not result.get("error") else ""
                return SkillResult(
                    success=True,
                    data={"title": title},
                    message=f"已前进到: {title}",
                )

            else:
                return SkillResult(success=False, error=f"未知导航操作: {action}")

        except Exception as e:
            logger.error(f"浏览器导航失败: {e}")
            return SkillResult(success=False, error=f"浏览器导航失败: {e}")


class BrowserCloseSkill(Skill):
    """
    关闭浏览器 - 关闭当前标签页或整个浏览器。
    """

    name = "browser_close"
    description = "关闭当前浏览器标签页或整个浏览器。"
    category = "browser"
    parameters = [
        SkillParameter("target", "string", "关闭目标: 'tab' 关闭当前标签页, 'browser' 关闭整个浏览器",
                        required=False, default="tab", enum=["tab", "browser"]),
    ]

    async def execute(self, target: str = "tab", **kwargs) -> SkillResult:
        """执行：关闭标签页或浏览器"""
        try:
            global _mcp_client
            if target == "browser":
                if _mcp_client:
                    await _mcp_client.stop()
                    _mcp_client = None
                return SkillResult(
                    success=True,
                    data={"action": "close_browser", "closed": True},
                    message="浏览器已完全关闭",
                )
            else:
                client = await get_mcp_client()
                result = await _call_mcp_with_retry("close_page", {}, timeout=15)
                return SkillResult(
                    success=True,
                    data={},
                    message="标签页已关闭",
                )
        except Exception as e:
            logger.error(f"关闭浏览器失败: {e}")
            return SkillResult(success=False, error=f"关闭失败: {e}")
