#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：smart_locate_service.py
模块描述：智能定位服务，自动访问被测系统、执行操作、提取选择器

作者：czj
创建日期：2026-08-05
最后修改：2026-08-05
"""

import logging
import os
import re
import base64
import json
from typing import List, Dict, Any, Optional
from datetime import datetime

from app.executors.playwright_executor import PlaywrightExecutor
from app.services.keyword_matcher import (
    extract_keywords,
    extract_value_from_description,
    detect_action_type,
    is_verification_step,
    match_element_by_keywords,
    find_element_by_semantic,
    get_candidate_details,  # 新增：获取候选元素详情
    _detect_parent_container,  # 阶段6升级：父容器推断（semantic.container 反推）
)
from app.services.selector_extractor import (
    extract_selectors,
    extract_element_info,
    build_step_with_selectors
)
from app.services.login_template_service import LoginTemplateService
from app.services.claude_service import ClaudeService  # 新增：Claude 服务
from app.config import settings

logger = logging.getLogger(__name__)


class SmartLocateService:
    """
    智能定位服务

    核心流程：
    1. 启动 Playwright 浏览器
    2. 执行登录模板（自动登录）
    3. 导航到目标菜单
    4. 逐步骤定位 + 执行
    5. 返回所有步骤的选择器

    特点：
    - 不依赖 Claude CLI，通过实际执行验证选择器
    - 支持登录模板自动执行
    - 支持功能中心导航
    - 提取多候选选择器，按优先级回退
    """

    def __init__(self):
        """初始化智能定位服务"""
        self.executor: Optional[PlaywrightExecutor] = None
        self.screenshot_dir = settings.SCREENSHOT_DIR
        self.use_claude: bool = True  # 是否启用 Claude 语义增强
        # ⚠️ 阶段6升级：语义化用例生成器依赖（懒加载，避免 import 循环）
        self._semantic_resolver: Optional[Any] = None
        self._page_url_service: Optional[Any] = None
        self._semantic_enabled_flag: Optional[bool] = None
        # 动态数据 CTX 化收集（fill 步骤值 → parameters 初值）
        self.collected_parameters: Dict[str, str] = {}
        # 动态值 → CTX key 去重映射（同值复用同一 key，保证创建/断言/清理同名）
        self._ctx_value_map: Dict[str, str] = {}

    def locate_steps(
        self,
        steps: List[Dict[str, Any]],
        auto_login: bool = True,
        navigate_menu: str = "",
        page_url: str = settings.TARGET_URL,
        use_claude: bool = True
    ) -> List[Dict[str, Any]]:
        """
        智能定位主流程

        Args:
            steps (List[Dict]): 步骤列表，格式：
                [
                    {"order": 1, "name": "输入用户名", "action": "fill", "params": {"value": "admin@xty"}},
                    {"order": 2, "name": "点击登录按钮", "action": "click", "params": {}}
                ]
            auto_login (bool): 是否自动登录
            navigate_menu (str): 目标菜单名称（如"信息发布"）
            page_url (str): 被测系统基础 URL
            use_claude (bool): 是否启用 Claude 语义增强，默认 True

        Returns:
            List[Dict]: 定位结果列表，格式：
                [
                    {
                        "order": 1,
                        "name": "输入用户名",
                        "success": True,
                        "action": "fill",
                        "params": {"selector": "...", "value": "admin@xty"},
                        "selectors": {...},
                        "screenshot": "base64..."
                    }
                ]
        """
        # 设置 Claude 增强开关
        self.use_claude = use_claude and settings.CLAUDE_ENABLED
        logger.info(f"开始智能定位: {len(steps)} 个步骤, auto_login={auto_login}, navigate_menu={navigate_menu}")

        # 阶段6升级：重置动态数据收集
        self.collected_parameters = {}
        self._ctx_value_map = {}

        results = []

        try:
            # 1. 启动 Playwright
            logger.info("启动 Playwright 浏览器...")
            self.executor = PlaywrightExecutor({
                'headless': settings.PLAYWRIGHT_HEADLESS,
                'timeout': 30000,
                'screenshot': True,
                'screenshot_dir': self.screenshot_dir,
                'ignore_https_errors': True,
            })
            self.executor.start()

            # 2. 自动登录
            if auto_login:
                logger.info("执行自动登录...")
                login_success = self._do_login_template()
                if not login_success:
                    logger.warning("自动登录失败，可能影响后续步骤定位")
                else:
                    logger.info("自动登录成功")
                    # ⚠️ 阶段6+：登录后等待首页 SPA 就绪（5.60 生产容器渲染较慢，
                    # 立即点击功能中心等菜单可能因前端事件尚未挂载而点击失效）
                    try:
                        self.executor._page.wait_for_timeout(3000)
                    except Exception:
                        pass

            # 3. 导航到目标菜单
            if navigate_menu:
                logger.info(f"导航到目标菜单: {navigate_menu}")
                nav_success = self._navigate_to_menu(navigate_menu)
                if not nav_success:
                    logger.warning(f"导航到菜单 '{navigate_menu}' 失败")

            # 4. 逐步骤定位
            skip_remaining = False
            seen_steps: List[Dict[str, Any]] = []  # 已完成步骤（Claude 上下文）
            for step in steps:
                # 如果前置步骤失败，跳过后续步骤
                if skip_remaining:
                    result = {
                        'order': step.get('order', 0),
                        'name': step.get('name', ''),
                        'success': False,
                        'action': step.get('action', ''),
                        'params': step.get('params', {}),
                        'selectors': {'primary': None, 'candidates': []},
                        'message': '前置步骤失败，跳过执行'
                    }
                    results.append(result)
                    continue

                # ⚠️ 阶段6+：auto_login 已自动登录，跳过用例中的登录步骤
                # （访问登录页/输入账号密码验证码/勾选协议/点击登录）。
                # 否则登录表单已消失 → 执行验证失败 → skip_remaining 中断整条定位链，
                # 导致表单步骤（如"时间切换为：预定会议"）永远无法被定位/升级 semantic。
                if auto_login and self._is_login_step(step):
                    logger.info(f"步骤 {step.get('order')} 已自动登录，跳过登录步骤")
                    results.append({
                        'order': step.get('order', 0),
                        'name': step.get('name', ''),
                        'success': True,
                        'action': step.get('action', ''),
                        'params': step.get('params', {}),
                        'selectors': {'primary': None, 'candidates': []},
                        'message': '已自动登录，跳过登录步骤'
                    })
                    seen_steps.append(step)
                    continue

                result = self._locate_single_step(step, previous_steps=seen_steps)
                results.append(result)
                seen_steps.append(step)

                # 如果定位成功且操作类型需要执行，则实际执行（确保页面状态正确）
                if result.get('success') and result.get('action') != 'navigate':
                    exec_success = self._execute_step(result)
                    if not exec_success:
                        # ⚠️ 阶段6+：已有选择器执行失败 → 清空选择器重新定位一次
                        # （旧用例的坏选择器如 div:visible:has-text 存在但不可点/语义无法升级时，
                        #  直接跳过后链必断；重定位走 keyword 路径产出新选择器 + semantic）
                        had_existing = bool((step.get('params') or {}).get('selector'))
                        if had_existing:
                            logger.warning(f"步骤 {step.get('order')} 已有选择器执行失败，尝试重新定位")
                            step_wo_sel = dict(step)
                            step_wo_sel['params'] = dict(step.get('params', {}))
                            step_wo_sel['params'].pop('selector', None)
                            try:
                                retry = self._locate_single_step(step_wo_sel, previous_steps=seen_steps)
                            except Exception as e:
                                logger.debug(f"步骤 {step.get('order')} 重新定位异常: {e}")
                                retry = None
                            if retry and retry.get('success') and self._execute_step(retry):
                                result = retry
                                result['message'] = f"重新定位成功: {result.get('message', '')}"
                                results[-1] = result
                                logger.info(f"步骤 {step.get('order')} 重新定位并执行成功")
                                # 替换结果后继续，不中断后续步骤
                                continue
                        # 执行失败，回退标记定位结果为失败
                        # 诊断 dump：记录执行失败时页面的真实状态（URL/表单值/表格行数），
                        # 用于排查异步加载时序导致的"元素暂时不存在"类失败
                        try:
                            _page = self.executor._page
                            _d = {'url': _page.url}
                            try:
                                _d['name_val'] = _page.locator('input[placeholder*="会议名称"]').input_value()
                            except Exception:
                                _d['name_val'] = 'NA'
                            try:
                                _d['table_rows'] = _page.locator('.meeting_check .el-table__row').count()
                                _d['target_rows'] = _page.locator('.meeting_check .el-table__row:has-text("北京展厅会议室")').count()
                                _d['table_text'] = (_page.locator('.meeting_check').inner_text() or '')[:150].replace('\n', ' | ')
                            except Exception as _e:
                                _d['table_err'] = str(_e)[:80]
                            logger.warning(f"STEP_FAIL_DUMP: {json.dumps(_d, ensure_ascii=False)}")
                        except Exception as _e:
                            logger.warning(f"STEP_FAIL_DUMP_ERR: {_e}")
                        logger.warning(f"步骤 {step.get('order')} 执行验证失败，回退标记定位为失败")
                        result['success'] = False
                        result['exec_success'] = False
                        result['message'] = f'定位成功但执行验证失败: {result.get("message", "")}'
                        # 后续步骤全部跳过
                        skip_remaining = True
                    else:
                        # ⚠️ 阶段6升级：执行成功后观察到确定状态变化才条件生成 verify
                        # （此时点击已发生，可检测成功提示/弹窗/抽屉；观察不到不生成，防假失败）
                        verify = self._detect_verify_after(step, self.executor._page)
                        if verify:
                            result['verify'] = verify
                            logger.info(f"步骤 {step.get('order')} 条件生成 verify: {verify}")

            logger.info(f"智能定位完成: {len(results)} 个步骤")

        except Exception as e:
            logger.error(f"智能定位异常: {e}")
            # 将异常信息添加到结果
            if not results:
                results.append({
                    'order': 0,
                    'name': '智能定位',
                    'success': False,
                    'message': f'定位异常: {str(e)}'
                })

        finally:
            # 5. 关闭浏览器
            if self.executor:
                try:
                    self.executor.stop()
                    logger.info("Playwright 浏览器已关闭")
                except Exception as e:
                    logger.warning(f"关闭浏览器失败: {e}")

        return results

    def _do_login_template(self) -> bool:
        """
        执行登录模板（使用 PlaywrightExecutor 内置的 do_login 方法）

        Returns:
            bool: 是否登录成功
        """
        try:
            return self.executor.do_login()
        except Exception as e:
            logger.error(f"执行登录模板失败: {e}")
            return False

    def _navigate_to_menu(self, menu_name: str) -> bool:
        """
        导航到目标菜单（功能中心 → 抽屉 → 目标菜单）
        支持二级菜单：先点击分类展开，再点击子菜单

        Args:
            menu_name (str): 目标菜单名称

        Returns:
            bool: 是否导航成功
        """
        # 二级菜单映射：子菜单 → 分类（使用 menu_mapping 统一配置）
        try:
            from app.utils.menu_mapping import get_menu_parent, is_direct_menu
            category = get_menu_parent(menu_name)
        except ImportError:
            # 回退：本地映射
            category_map = {
                '资产信息': '资产管理', '资产故障': '资产管理', '资产设备': '资产管理',
                '壁纸推送': '集控控制', '脚本命令': '集控控制', '应用管理': '集控控制',
                '屏幕管理': '集控控制', '软件卸载': '集控控制', '设备网络': '集控控制',
                '门口屏发布': '集控控制', '播放器发布': '集控控制',
                '运维设备': '会议运维', '运维巡检': '会议运维',
                '工单列表': '维护工单', '我的工单': '维护工单', '告警工单': '维护工单',
                '信息窗管理': '信息管理', '消息通知': '信息管理',
                '会务统筹': '会务管理', '会务工单': '会务管理',
            }
            category = category_map.get(menu_name)

        try:
            page = self.executor._page

            # 步骤 1: 点击功能中心图标
            logger.debug("点击功能中心图标...")

            # 功能中心选择器候选列表（按优先级排序）
            func_center_selectors = [
                "//*[@id='Home']/div[1]/div[1]/i",   # XPath（已验证）
                "//*[@id='Home']/div[1]/div[1]",      # XPath div
                ".home_nav_left",                      # class 选择器
            ]

            func_center_clicked = False
            for sel in func_center_selectors:
                try:
                    page.click(sel, timeout=10000)
                    func_center_clicked = True
                    logger.info(f"成功点击功能中心图标: {sel}")
                    break
                except Exception as e:
                    logger.debug(f"功能中心选择器 {sel} 点击失败: {e}")
                    continue

            # 回退：JS click（处理元素在负坐标或被遮挡的情况）
            if not func_center_clicked:
                try:
                    page.evaluate("""
                        () => {
                            const el = document.querySelector('.home_nav_left')
                                || document.querySelector('[id="Home"] > div:first-child > div:first-child');
                            if (el) { el.click(); return true; }
                            return false;
                        }
                    """)
                    func_center_clicked = True
                    logger.info("成功点击功能中心图标（JS click 回退）")
                except Exception as e2:
                    logger.error(f"无法点击功能中心图标（所有方式均失败）: {e2}")
                    return False

            # 步骤 2: 等待功能抽屉打开并完成动画
            logger.debug("等待功能抽屉打开...")
            try:
                # 2.1 等待抽屉元素出现
                page.wait_for_selector('.el-drawer', timeout=5000)
                logger.debug("抽屉元素已出现")

                # 2.2 等待抽屉动画完成（检测抽屉是否展开）
                # Element Plus 抽屉展开动画约 300ms
                import time
                time.sleep(0.3)

                # 2.3 确认抽屉已完全展开
                for _ in range(10):  # 最多等待 1 秒
                    try:
                        drawer = page.query_selector('.el-drawer')
                        if drawer:
                            # 检查抽屉是否展开（宽度大于 0 且可见）
                            is_open = drawer.evaluate("""
                                el => {
                                    const style = window.getComputedStyle(el);
                                    const width = parseFloat(style.width);
                                    return width > 100;  // 抽屉展开后宽度应该大于 100px
                                }
                            """)
                            if is_open:
                                logger.debug("抽屉已完全展开")
                                break
                    except Exception:
                        pass
                    time.sleep(0.1)

                # 2.4 额外稳定等待
                time.sleep(0.2)

            except Exception as e:
                logger.warning(f"等待抽屉超时: {e}，继续尝试后续步骤")

            # 步骤 3: 如果是二级菜单，先点击分类展开
            if category:
                logger.debug(f"二级菜单导航: {category} -> {menu_name}")
                try:
                    category_selector = f'.el-drawer >> text="{category}"'
                    # 直接尝试点击（page.click 内部会等待元素出现）
                    page.click(category_selector, timeout=8000)
                    page.wait_for_timeout(800)
                    logger.info(f"成功点击分类: {category}")
                except Exception as e:
                    logger.warning(f"点击分类失败: {e}，尝试直接点击菜单")

            # 步骤 4: 点击目标菜单（带滚动和重试）
            logger.debug(f"点击目标菜单: {menu_name}")

            # 菜单选择器候选列表（按优先级排序）
            menu_selectors = [
                f'.el-drawer >> text="{menu_name}"',           # 优先：直接文本匹配
                f'.el-drawer a:has-text("{menu_name}")',       # 备选：链接元素
                f'.el-drawer button:has-text("{menu_name}")',  # 备选：按钮元素
                f'.el-drawer [role="menuitem"]:has-text("{menu_name}")',  # 备选：菜单项
            ]

            click_success = False
            for i, menu_selector in enumerate(menu_selectors):
                try:
                    logger.debug(f"尝试选择器 {i+1}/{len(menu_selectors)}: {menu_selector}")

                    # 直接尝试点击（page.click 内部会等待元素出现）
                    page.click(menu_selector, timeout=8000)
                    click_success = True
                    logger.info(f"成功点击菜单（选择器 {i+1}）: {menu_name}")
                    break

                except Exception as e:
                    logger.warning(f"选择器 {i+1} 点击失败: {e}")
                    continue

            if click_success:
                page.wait_for_timeout(1500)  # 等待页面加载
                logger.info(f"成功导航到菜单: {menu_name}")
                return True
            else:
                logger.error(f"所有选择器都无法点击菜单: {menu_name}")
                return False

        except Exception as e:
            logger.error(f"导航到菜单异常: {e}")
            return False

    def _locate_single_step(
        self,
        step: Dict[str, Any],
        previous_steps: Optional[List[Dict[str, Any]]] = None,
    ) -> Dict[str, Any]:
        """
        为单个步骤定位元素

        Args:
            step (Dict): 步骤定义
            previous_steps (Optional[List[Dict]]): 当前步骤之前的步骤（Claude 语义排序上下文）

        Returns:
            Dict: 定位结果
        """
        order = step.get('order', 0)
        name = step.get('name', '')
        action = step.get('action', '')
        params = step.get('params', {})

        logger.info(f"定位步骤 {order}: {name} (action={action})")

        result = {
            'order': order,
            'name': name,
            'success': False,
            'action': action,
            'params': params,
            'selectors': {'primary': None, 'candidates': []},
            'element_info': {},
            'screenshot': None,
            'message': ''
        }

        try:
            # ⚠️ 关键修复：先获取 page 对象，验证步骤需要使用
            page = self.executor._page

            # ⚠️ 阶段6+：navigate 步骤优先处理（selector 可能是 URL 而非 CSS 选择器，
            # 不能走 wait_for_selector 校验，否则 URL 被当 CSS 解析报 "Unexpected token /"）
            if action == 'navigate':
                # 检查是否已有 url 参数（params.url 或 params.selector 里的 http 地址）
                url = params.get('url', '') or ''
                if not url:
                    sel_candidate = str(params.get('selector', '') or '')
                    if 'http' in sel_candidate:
                        url = sel_candidate
                if not url:
                    # 从步骤名称中提取 URL 或使用默认值
                    import re
                    url_match = re.search(r'https?://[^\s]+', name)
                    if url_match:
                        url = url_match.group(0)
                    else:
                        # 使用默认 URL
                        url = settings.TARGET_URL

                result['success'] = True
                result['params']['url'] = url
                result['selectors'] = {'primary': url, 'candidates': []}
                result['message'] = f'导航步骤: {url}'
                logger.info(f"步骤 {order} 导航到: {url}")
                return result

            # ⚠️ 新增：如果步骤已有选择器，直接使用，不重新定位
            existing_selector = params.get('selector', '')
            if existing_selector:
                logger.info(f"步骤 {order} 已有选择器: {existing_selector}，直接使用")
                result['success'] = True
                result['selectors'] = {
                    'primary': existing_selector,
                    'candidates': [{'type': 'css', 'value': existing_selector, 'confidence': 0.95, 'priority': 1}]
                }
                result['message'] = f'使用已有选择器: {existing_selector}'

                # ⚠️ 阶段6+：已有选择器步骤同样尝试语义化升级（生成 semantic + 前置已验证解析链），
                # 让旧用例重跑智能定位即可升级为语义化执行。不改写用户 selector，保留为兜底。
                if self._semantic_enabled():
                    self._try_attach_semantic(result, step, page)

                # 验证选择器是否有效（增加等待时间）
                try:
                    page.wait_for_selector(existing_selector, timeout=8000)
                    logger.info(f"步骤 {order} 已有选择器验证通过")
                    return result
                except Exception as e:
                    logger.warning(f"步骤 {order} 已有选择器验证失败: {e}，仍使用原选择器")
                    # 选择器验证失败，但仍然使用原选择器（可能是页面状态问题）
                    # 不再尝试智能定位，避免覆盖用户手动配置的选择器
                    result['message'] = f'使用已有选择器（验证超时）: {existing_selector}'
                    return result

            # ⚠️ 新增：登录相关步骤优先使用已验证的选择器
            login_selectors = self._get_login_selector(name)
            if login_selectors:
                result['success'] = True
                result['selectors'] = {
                    'primary': login_selectors,
                    'candidates': [{'type': 'css', 'value': login_selectors, 'confidence': 0.95, 'priority': 1}]
                }
                result['params']['selector'] = login_selectors
                result['message'] = f'登录模板选择器: {login_selectors}'
                logger.info(f"步骤 {order} 使用登录模板选择器: {login_selectors}")

                # 自动修正 fill 步骤的 value
                if action == 'fill':
                    extracted_value = extract_value_from_description(name)
                    if extracted_value:
                        result['params']['value'] = extracted_value
                        logger.info(f"步骤 {order} 自动修正 value: '{params.get('value', '')}' -> '{extracted_value}'")

                return result

            # ⚠️ 新增：验证类步骤特殊处理
            # 验证类步骤（如"查看是否新增成功"）支持两种验证方式：
            # 1. 提示语验证：等待成功提示出现
            # 2. 列表搜索验证：在列表中搜索判断是否新增
            if is_verification_step(name):
                logger.info(f"步骤 {order} 为验证类步骤，尝试查找操作结果")

                # ===== 方式1：提示语验证 =====
                message_selectors = [
                    '.el-message--success .el-message__content',  # 成功提示
                    '.el-message__content',  # 通用提示内容
                    '//p[@class="el-message__content"]',  # XPath 方式
                    '.el-message',  # 整个提示框
                ]

                for sel in message_selectors:
                    try:
                        # 等待提示出现（操作后通常会有短暂延迟）
                        page.wait_for_selector(sel, timeout=5000)
                        element = page.locator(sel).first
                        if element:
                            # 提取提示文本用于验证
                            message_text = element.inner_text().strip()
                            logger.info(f"找到操作结果提示: '{message_text}'")

                            result['success'] = True
                            result['action'] = 'wait'  # 改为 wait 类型
                            result['selectors'] = {
                                'primary': sel,
                                'candidates': [
                                    {'type': 'css', 'value': sel, 'confidence': 0.95, 'priority': 1}
                                ]
                            }
                            result['params']['selector'] = sel
                            result['params']['timeout'] = 10000
                            result['params']['expected_text'] = message_text  # 记录提示文本
                            result['params']['verify_method'] = 'message'  # 验证方式
                            result['message'] = f'验证类步骤（提示语），找到提示: {message_text}'
                            logger.info(f"步骤 {order} 定位成功（提示验证）: {sel}")

                            # 截图
                            try:
                                screenshot_path = os.path.join(
                                    self.screenshot_dir,
                                    f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
                                )
                                page.screenshot(path=screenshot_path)
                                with open(screenshot_path, 'rb') as f:
                                    result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
                            except Exception as e:
                                logger.debug(f"截图失败（非致命）: {e}")

                            return result
                    except Exception as e:
                        logger.debug(f"提示语选择器 {sel} 未找到: {e}")
                        continue

                logger.info(f"未找到提示语，尝试列表搜索验证...")

                # ===== 方式2：列表搜索验证 =====
                # 查找页面上的搜索输入框
                search_input_selectors = [
                    'input[placeholder*="搜索"]',
                    'input[placeholder*="查询"]',
                    'input[placeholder*="查找"]',
                    'input[placeholder*="关键字"]',
                    '.search-input',
                    '.el-input__inner',
                ]

                for sel in search_input_selectors:
                    try:
                        element = page.locator(sel).first
                        if element and element.is_visible():
                            logger.info(f"找到搜索输入框: {sel}")

                            result['success'] = True
                            result['action'] = 'fill'  # 改为 fill 类型（输入搜索词）
                            result['selectors'] = {
                                'primary': sel,
                                'candidates': [
                                    {'type': 'css', 'value': sel, 'confidence': 0.85, 'priority': 1}
                                ]
                            }
                            result['params']['selector'] = sel
                            # 从步骤名称中提取搜索关键词（如"查看列表是否正确新增数据"）
                            # 用户需要手动指定搜索词，这里只提供选择器
                            result['params']['verify_method'] = 'search'  # 验证方式
                            result['message'] = f'验证类步骤（列表搜索），找到搜索框: {sel}'
                            logger.info(f"步骤 {order} 定位成功（列表搜索）: {sel}")

                            # 截图
                            try:
                                screenshot_path = os.path.join(
                                    self.screenshot_dir,
                                    f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
                                )
                                page.screenshot(path=screenshot_path)
                                with open(screenshot_path, 'rb') as f:
                                    result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
                            except Exception as e:
                                logger.debug(f"截图失败（非致命）: {e}")

                            return result
                    except Exception as e:
                        logger.debug(f"搜索框选择器 {sel} 未找到: {e}")
                        continue

                # ===== 方式3：目标文本可见验证 =====
                # 详情页/展示页场景：无提示语、无搜索框，验证对象是页面上的目标文本
                # （如"查看界面会议名称正确显示：自动化新建会议"→ 等待文本"自动化新建会议"出现）
                # 从步骤名提取 "：X" 尾部值作为目标文本，等待 :has-text 出现即验证通过
                # 适用：创建成功后跳转详情页/展示页，验证动态值（名称/编号）已在页面正确显示
                _text_m = re.search(r'[：:]\s*([^：:]+?)\s*$', name)
                if _text_m and _text_m.group(1).strip():
                    target_text = _text_m.group(1).strip()
                    # 过滤明显非文本值：过长描述/元素类词/含引号文本不作为目标文本
                    if (len(target_text) <= 30 and '"' not in target_text and '\\' not in target_text
                            and not any(w in target_text for w in ('按钮', '弹窗', '搜索框', '下拉框'))):
                        text_sel = f':has-text("{target_text}")'
                        try:
                            page.wait_for_selector(text_sel, timeout=20000)
                            logger.info(f"找到目标文本（详情页验证）: {target_text}")
                            result['success'] = True
                            result['action'] = 'wait'  # wait 类型：等待目标文本出现即验证通过
                            result['selectors'] = {
                                'primary': text_sel,
                                'candidates': [
                                    {'type': 'css', 'value': text_sel, 'confidence': 0.90, 'priority': 1}
                                ]
                            }
                            result['params']['selector'] = text_sel
                            result['params']['timeout'] = 20000
                            result['params']['verify_method'] = 'text_visible'
                            result['params']['expected_text'] = target_text  # 记录目标文本
                            result['message'] = f'验证类步骤（目标文本可见），找到文本: {target_text}'
                            logger.info(f"步骤 {order} 定位成功（目标文本验证）: {text_sel}")

                            # 截图
                            try:
                                screenshot_path = os.path.join(
                                    self.screenshot_dir,
                                    f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
                                )
                                page.screenshot(path=screenshot_path)
                                with open(screenshot_path, 'rb') as f:
                                    result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
                            except Exception as e:
                                logger.debug(f"截图失败（非致命）: {e}")

                            return result
                        except Exception as e:
                            logger.debug(f"目标文本 {target_text} 未出现: {e}")

                # 如果三种方式都没有找到
                logger.warning(f"步骤 {order} 为验证类但未找到提示元素或搜索框")
                result['success'] = False
                result['message'] = f'验证类步骤，但未找到操作结果提示元素或列表搜索框'
                return result

            # ⚠️ 阶段6升级：语义解析优先（反向复用执行期解析链）
            # 静态步骤（无动态值）先构造候选 semantic → resolve_step 命中 → 实际执行验证
            # 成功 = 该 semantic 目标已通过执行期解析器验证，落库后执行必稳
            # 动态值步骤（含"xxx：值"）不走此路径（避免语义键被值污染），由 keyword 定位后 _infer 反推
            if self._semantic_enabled():
                sem_result = self._try_semantic_execute(page, step, self._get_page_scope(page))
                if sem_result:
                    chain = sem_result['chain']
                    result['success'] = True
                    result['selectors'] = {
                        'primary': chain[0],
                        'candidates': [
                            {'type': 'css', 'value': sel, 'confidence': 0.95, 'priority': i + 1}
                            for i, sel in enumerate(chain)
                        ]
                    }
                    result['params']['selector'] = chain[0]
                    result['semantic'] = sem_result['semantic']
                    result['message'] = f'语义解析优先定位成功: {chain[0]}'
                    logger.info(f"步骤 {order} 语义解析优先命中: {result['semantic']}")

                    # ⚠️ 阶段6升级：fill 动态值同样 CTX 化（语义优先路径同样收集 parameters 初值）
                    if action == 'fill' and result['params'].get('value'):
                        self._ctf_param(result['params'])

                    # 截图
                    self._capture_screenshot(page, order, result)
                    return result

            # 提取关键词
            keywords = extract_keywords(name)
            logger.debug(f"提取关键词: {keywords}")

            # ⚠️ 新增：步骤模式预处理
            # 识别"xxx选择：yyy"（列表复选框）和"xxx切换为：yyy"（Tab切换）模式
            pattern_result = self._preprocess_step_pattern(name, action)
            if pattern_result and pattern_result['selectors']:
                result['success'] = True
                result['selectors'] = {
                    'primary': pattern_result['selectors'][0],
                    'candidates': [
                        {'type': 'css', 'value': sel, 'confidence': pattern_result['confidence'], 'priority': i + 1}
                        for i, sel in enumerate(pattern_result['selectors'])
                    ]
                }
                result['params']['selector'] = pattern_result['selectors'][0]
                result['message'] = f"模式匹配成功: {pattern_result['type']}"
                logger.info(f"步骤 {order} 匹配特殊模式: {pattern_result['type']}")

                # ⚠️ 阶段6升级：模式步骤同样生成 semantic（row_checkbox/tab + contains 锚点值）
                if self._semantic_enabled():
                    semantic = self._infer_semantic_from_pattern(step, pattern_result, page)
                    if semantic:
                        result['semantic'] = semantic
                        logger.info(f"步骤 {order} 模式步骤生成 semantic: {semantic}")

                # 截图
                self._capture_screenshot(page, order, result)
                return result

            # 第一级：关键词直接匹配（返回候选列表）
            # Claude 语义增强已能理解元素上下文，不再需要手动区分抽屉内/外
            elements_list, selectors = match_element_by_keywords(page, keywords, action)

            # ⚠️ 阶段6升级：定位成功元素引用（keyword 成功后反向生成 semantic 用）
            located_element = None

            if elements_list and selectors:
                # === Phase 5: Claude 语义增强（单候选也验证） ===
                if self.use_claude and len(elements_list) >= 1:
                    try:
                        logger.info(f"步骤 {order} 启用 Claude 语义增强，候选数: {len(elements_list)}")

                        # 获取候选元素详情
                        candidates = get_candidate_details(page, elements_list)

                        # Phase 5: 传递页面上下文和步骤上下文
                        page_url = page.url if page else ""
                        previous_steps_info = []
                        # 获取前面步骤的名称作为上下文
                        # （修复：原 self.steps[:step_idx] 引用未定义的 self.steps/step_idx，
                        #  导致 Claude 增强路径恒抛异常回退；改为方法参数传入）
                        for prev_step in (previous_steps or []):
                            previous_steps_info.append({
                                'name': prev_step.get('name', ''),
                                'action': prev_step.get('action', '')
                            })

                        # 调用 Claude 服务进行语义排序
                        claude_service = ClaudeService()
                        selected_idx, confidence, reason, confirmed = claude_service.rank_candidates(
                            step_name=name,
                            action=action,
                            params=params,
                            candidates=candidates,
                            page_url=page_url,
                            previous_steps=previous_steps_info
                        )

                        # Phase 5: 处理 Claude 返回结果
                        if confirmed and 0 <= selected_idx < len(selectors):
                            primary_selector = selectors[selected_idx]['value']
                            result['success'] = True
                            result['selectors'] = {
                                'primary': primary_selector,
                                'candidates': selectors
                            }
                            result['claude_enhanced'] = True
                            result['claude_confidence'] = confidence
                            result['claude_reason'] = reason
                            result['message'] = f'Claude语义增强定位成功: {primary_selector}'
                            logger.info(f"步骤 {order} Claude 确认: index={selected_idx}, confidence={confidence:.2f}, reason={reason}")
                        elif not confirmed:
                            # Claude 认为当前候选都不匹配
                            # 仍然使用第一个候选，但标记为未确认
                            primary_selector = selectors[0]['value']
                            result['success'] = True
                            result['selectors'] = {'primary': primary_selector, 'candidates': selectors}
                            result['claude_enhanced'] = True
                            result['claude_confidence'] = confidence
                            result['claude_reason'] = reason
                            result['claude_confirmed'] = False
                            result['message'] = f'Claude未确认最佳候选，回退到第一个: {primary_selector}'
                            logger.warning(f"步骤 {order} Claude 未确认: reason={reason}")
                        else:
                            # Claude 返回索引无效，回退到第一个
                            primary_selector = selectors[0]['value']
                            result['success'] = True
                            result['selectors'] = {'primary': primary_selector, 'candidates': selectors}
                            result['claude_enhanced'] = False
                            result['message'] = f'关键词匹配成功（Claude返回无效索引）: {primary_selector}'
                            logger.warning(f"步骤 {order} Claude 返回索引无效: {selected_idx}")

                    except Exception as e:
                        # Claude 失败，回退到第一个候选
                        logger.warning(f"步骤 {order} Claude 语义增强失败，回退: {e}")
                        primary_selector = selectors[0]['value']
                        result['success'] = True
                        result['selectors'] = {'primary': primary_selector, 'candidates': selectors}
                        result['claude_enhanced'] = False
                        result['message'] = f'关键词匹配成功（Claude回退）: {primary_selector}'

                # 未启用 Claude，直接使用第一个
                elif not self.use_claude:
                    primary_selector = selectors[0]['value']
                    result['success'] = True
                    result['selectors'] = {'primary': primary_selector, 'candidates': selectors}
                    result['claude_enhanced'] = False
                    result['message'] = f'关键词匹配成功: {primary_selector}'
                    logger.info(f"步骤 {order} 定位成功（关键词匹配）: {primary_selector}")

                # 兜底
                else:
                    primary_selector = selectors[0]['value']
                    result['success'] = True
                    result['selectors'] = {'primary': primary_selector, 'candidates': selectors}
                    result['claude_enhanced'] = False
                    result['message'] = f'关键词匹配成功: {primary_selector}'
                    logger.info(f"步骤 {order} 定位成功（默认）: {primary_selector}")

                # 提取元素信息（使用第一个元素）
                if elements_list:
                    result['element_info'] = extract_element_info(elements_list[0])
                    located_element = elements_list[0]

            else:
                # 第二级：语义推断（回退策略，只返回单一元素）
                # （函数可能返回 None，防御性解包避免定位异常）
                _sem_infer = find_element_by_semantic(page, keywords, action) or (None, [])
                element, selectors = _sem_infer

                if element and selectors:
                    result['success'] = True
                    primary_selector = selectors[0]['value'] if selectors else None
                    result['selectors'] = {
                        'primary': primary_selector,
                        'candidates': selectors
                    }
                    result['element_info'] = extract_element_info(element)
                    located_element = element
                    result['claude_enhanced'] = False
                    result['message'] = f'语义推断成功: {primary_selector}'
                    logger.info(f"步骤 {order} 定位成功（语义推断）: {primary_selector}")
                else:
                    result['success'] = False
                    result['message'] = f'未找到匹配元素: {name}'
                    logger.warning(f"步骤 {order} 定位失败: {result['message']}")

            # 更新 params 中的 selector
            if result['success'] and result['selectors'].get('primary'):
                result['params']['selector'] = result['selectors']['primary']

            # ⚠️ 自动修正 fill 步骤的 value
            if action == 'fill' and result['success']:
                current_value = result['params'].get('value', '')
                # 从步骤名称中提取正确的 value
                extracted_value = extract_value_from_description(name)
                if extracted_value and extracted_value != current_value:
                    logger.info(f"步骤 {order} 自动修正 value: '{current_value}' -> '{extracted_value}'")
                    result['params']['value'] = extracted_value

            # ⚠️ 阶段6升级：keyword 定位成功后反向生成 semantic 目标
            # 该 semantic 已通过"实际定位 + 元素信息"双重验证，落库后走语义化执行机制
            # （verify 由 locate_steps 在执行成功后统一条件生成，见 _detect_verify_after 调用点）
            if result['success'] and result['selectors'].get('primary') and located_element is not None:
                semantic = self._infer_semantic_from_element(step, located_element, page)
                if semantic:
                    result['semantic'] = semantic
                    logger.info(f"步骤 {order} 反向生成 semantic: {semantic}")

            # ⚠️ 阶段6升级：fill 步骤动态值 CTX 化（收集到 parameters 初值）
            if action == 'fill' and result['success'] and result['params'].get('value'):
                self._ctf_param(result['params'])

            # 截图
            try:
                screenshot_path = os.path.join(
                    self.screenshot_dir,
                    f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
                )
                page.screenshot(path=screenshot_path)
                with open(screenshot_path, 'rb') as f:
                    result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
            except Exception as e:
                logger.debug(f"截图失败（非致命）: {e}")

        except Exception as e:
            result['success'] = False
            result['message'] = f'定位异常: {str(e)}'
            logger.error(f"步骤 {order} 定位异常: {e}")

        return result

    def _execute_step(self, result: Dict[str, Any]) -> bool:
        """
        实际执行步骤（验证选择器是否有效）
        如果点击后预期弹窗未出现，自动尝试替代选择器

        Args:
            result (Dict): 定位结果（包含 selector 和 params）

        Returns:
            bool: 是否执行成功
        """
        action = result.get('action', '')
        params = result.get('params', {})
        selector = params.get('selector', '')
        step_name = result.get('name', '')
        order = result.get('order', 0)

        if not selector:
            return False

        logger.debug(f"执行步骤验证: action={action}, selector={selector}")

        try:
            page = self.executor._page

            if action == 'fill':
                value = params.get('value', '')
                page.wait_for_selector(selector, timeout=5000)
                page.fill(selector, str(value))
                logger.debug(f"填充成功: {selector} = {value}")
                return True

            elif action == 'click':
                # 判断点击后是否预期打开弹窗/抽屉
                dialog_keywords = ['新建', '编辑', '添加', '新增', '创建', '修改', '详情',
                                   '确定创建', '取消会议', '完成']
                expects_dialog = any(kw in step_name for kw in dialog_keywords)

                # 构建替代选择器列表（优先级从高到低）：
                # 主选择器 + 定位结果中的完整候选列表（pattern/语义路径会产出多个候选，
                # 仅尝试主选择器会漏掉真正可用的候补，如列表行的 tr:has-text 兜底）
                click_selector_chain = [selector]
                for cand in result.get('selectors', {}).get('candidates', []):
                    v = cand.get('value') if isinstance(cand, dict) else cand
                    if v and v != selector and v not in click_selector_chain:
                        click_selector_chain.append(str(v))

                # 如果当前选择器是 div:has-text 格式，尝试补充 button:has-text
                if 'div:visible:has-text' in selector or 'div:has-text' in selector:
                    import re
                    text_match = re.search(r'has-text\("([^"]+)"\)', selector)
                    if text_match:
                        btn_text = text_match.group(1)
                        click_selector_chain.append(f'button:has-text("{btn_text}")')
                        click_selector_chain.append(f'[role="button"]:has-text("{btn_text}")')
                        click_selector_chain.append(f'.el-button:has-text("{btn_text}")')

                # 如果当前选择器是 input:visible（错误匹配），尝试从步骤名提取关键词
                if 'input:visible' in selector:
                    click_selector_chain.append(f'button:has-text("{step_name}")')

                # 点击前等待目标文本渲染：列表/表格异步加载兜底
                # （旧 case 已有选择器如 .el-checkbox:has-text 常匹配 0 个；若表格数据仍在异步加载，
                #  直接点击/重定位都会因"目标行暂不存在"失败——先等 has-text 目标文本出现，
                #  确保后续点击链与 Fix5 重定位时表格已就绪）
                try:
                    _tm = re.search(r'has-text\("([^"]+)"\)', selector)
                    if _tm:
                        # 用 :has-text 子串匹配更宽容：TD 内文本可能带空白/嵌套，
                        # 精确 text= 匹配常漏掉导致"表格已加载但等待超时"
                        # 目标系统(192.168.5.44)表格异步加载极慢且不稳，放宽到 25s
                        page.wait_for_selector(f':has-text("{_tm.group(1)}")', timeout=25000)
                        page.wait_for_timeout(300)
                        logger.debug(f"步骤 {order} 目标文本已渲染: {_tm.group(1)}")
                except Exception:
                    pass

                # 逐级尝试点击（每级：普通点击 → force 强点；缩短超时避免 30s 默认卡死）
                # 记录点击前 URL，用于判断是否为整页路由跳转（跳转则无需等待弹窗）
                url_before = ''
                try:
                    url_before = page.url
                except Exception:
                    pass
                click_success = False
                actual_selector_used = None
                for sel in click_selector_chain:
                    try:
                        page.wait_for_selector(sel, timeout=5000)
                        page.click(sel, timeout=5000)
                        page.wait_for_timeout(800)
                        logger.debug(f"点击成功: {sel}")
                        click_success = True
                        actual_selector_used = sel
                        break
                    except Exception as e:
                        # 普通点击失败（元素被遮挡/滚动区/等），重试 force 点击
                        # （对齐示范用例"勾选会议室 force=True"的经验：列表行勾选常需强点）
                        try:
                            page.click(sel, force=True, timeout=5000)
                            page.wait_for_timeout(800)
                            logger.debug(f"点击成功(force): {sel}")
                            click_success = True
                            actual_selector_used = sel
                            break
                        except Exception as e2:
                            # JS 直接触发 click（绕过 playwright actionability 检测）：
                            # Element Plus 表格 checkbox 等元素普通/force 点击常超时
                            # （实测 .el-checkbox:has-text 匹配 0 个、JS el.click() 可成功勾选），
                            # 作为最后兜底（取第一个匹配）
                            try:
                                page.locator(sel).first.evaluate('el => el.click()')
                                page.wait_for_timeout(800)
                                logger.debug(f"点击成功(js): {sel}")
                                click_success = True
                                actual_selector_used = sel
                                break
                            except Exception as e3:
                                logger.debug(f"选择器 {sel} 点击失败（普通+force+js）: {e3}")
                                continue

                if not click_success:
                    logger.warning(f"步骤 {order} 所有选择器点击均失败")
                    return False

                # 更新 params 中的 selector 为实际使用的选择器
                if actual_selector_used != selector:
                    result['params']['selector'] = actual_selector_used
                    logger.info(f"步骤 {order} 选择器已更新: {selector} -> {actual_selector_used}")

                # === 页面状态验证 ===
                # 1. 检测是否是功能中心点击
                if '功能中心' in step_name:
                    logger.debug("检测到功能中心点击，等待抽屉打开...")
                    try:
                        page.wait_for_selector('.el-drawer', timeout=8000)
                        page.wait_for_timeout(500)
                        logger.debug("功能中心抽屉已打开")
                    except Exception as e:
                        logger.warning(f"功能中心抽屉未打开，标记为失败: {e}")
                        return False

                # 2. 检测弹窗是否出现（如果步骤名称包含弹窗关键词）
                if expects_dialog:
                    # 整页路由跳转豁免：步骤名含"新建/创建"等弹窗关键词，但实际点击是页面跳转
                    # （如 新建会议 → /meetingV3 整页路由，无 .el-dialog/.el-drawer），URL 变化视为成功
                    url_after = ''
                    try:
                        url_after = page.url
                    except Exception:
                        pass
                    url_navigated = bool(url_before and url_after and url_before != url_after)
                    if url_navigated:
                        logger.info(f"步骤 '{step_name}' 点击后 URL 变化（页面跳转），豁免弹窗检查: {url_before} -> {url_after}")
                        return True

                    dialog_opened = False
                    for dialog_sel in ['.el-dialog', '.el-drawer', '.el-message-box']:
                        try:
                            page.wait_for_selector(dialog_sel, timeout=3000)
                            dialog_opened = True
                            logger.debug(f"弹窗已打开: {dialog_sel}")
                            break
                        except Exception:
                            continue

                    if not dialog_opened:
                        logger.warning(f"步骤 '{step_name}' 点击后未检测到弹窗，所有选择器均失败")
                        # 重要：弹窗未打开，返回 False 触发 skip_remaining
                        return False

                # 3. 检测页面是否跳转（如果步骤名称包含导航关键词）
                nav_keywords = ['进入', '打开', '跳转', '导航']
                if any(kw in step_name for kw in nav_keywords):
                    page.wait_for_timeout(1000)

                return True

            elif action == 'wait':
                timeout = params.get('timeout', 10000)
                page.wait_for_selector(selector, timeout=timeout)
                logger.debug(f"等待成功: {selector}")
                return True

            elif action == 'select':
                value = params.get('value', '')
                page.wait_for_selector(selector, timeout=5000)
                page.select_option(selector, str(value))
                logger.debug(f"选择成功: {selector} = {value}")
                return True

            return True

        except Exception as e:
            logger.warning(f"执行步骤验证失败: {e}")
            return False

    # ==================== 阶段6升级：语义化用例生成 ====================

    # 前导动作动词（语义文本提取时剔除，长词在前避免短词误剥）
    _ACTION_VERBS = [
        '点击', '输入', '填写', '键入', '选择', '打开', '进入', '切换到', '切换为', '切换',
        '取消勾选', '勾选', '拖动', '滚动', '悬停', '等待', '查看', '展开', '收起', '刷新',
    ]

    def _semantic_enabled(self) -> bool:
        """语义化生成器是否可用（懒加载，避免 import 循环；依赖缺失降级为旧路径）"""
        if self._semantic_enabled_flag is None:
            try:
                from app.services.semantic_resolver import get_semantic_resolver
                self._semantic_resolver = get_semantic_resolver()
                from app.services.page_url_service import get_page_url_service
                self._page_url_service = get_page_url_service()
                self._semantic_enabled_flag = True
            except Exception as e:
                logger.warning(f"语义化生成器不可用，降级为旧 keyword 路径: {e}")
                self._semantic_enabled_flag = False
        return self._semantic_enabled_flag

    def _get_page_scope(self, page) -> Optional[str]:
        """运行时页面身份识别（复用执行期 recognize_current_page）"""
        if not self._semantic_enabled() or page is None:
            return None
        try:
            rec = self._page_url_service.recognize_current_page(page)
            scope_id = rec.get("id") if rec else None
            if scope_id:
                logger.debug(f"页面身份识别: {rec.get('name')}({scope_id})")
            else:
                logger.debug("页面身份未识别，semantic.scope 留空（执行期降级安全）")
            return scope_id
        except Exception as e:
            logger.debug(f"页面身份识别异常: {e}")
            return None

    def _build_candidate_semantic(self, step: Dict[str, Any], scope: Optional[str]) -> Optional[Dict[str, Any]]:
        """
        从步骤名/action 构造候选 semantic 目标（语义解析优先路径用）
        仅静态步骤（无"xxx：值"）调用——动态步骤由 keyword 定位后 _infer 反推
        """
        name = step.get('name', '')
        action = step.get('action', '')
        text = self._extract_semantic_text(name)
        if not text:
            return None
        semantic: Dict[str, Any] = {
            'type': self._infer_element_type(action, None),
            'text': text,
        }
        if scope:
            semantic['scope'] = scope
        nth = self._extract_nth(name)
        if nth is not None:
            semantic['nth'] = nth
        return semantic

    def _try_attach_semantic(self, result: Dict[str, Any], step: Dict[str, Any], page) -> None:
        """
        已有选择器步骤的语义化升级：生成 semantic + 前置已验证的解析链

        场景：旧用例所有步骤都有 params.selector（智能定位旧版产物），
        _locate_single_step 的"已有选择器"分支会 early return，从不生成 semantic，
        导致这些步骤永远停留在"裸选择器"执行，遇到 DOM 变更即失败（如自定义 tab）。

        本方法仅附加 semantic 目标（不改写用户手动配置的 selector，保留为兜底），
        并把语义解析链置于 candidates / params.selector 最前：
        - 智能定位自身的 _execute_step 验证先点语义链 → 验证通过，链不中断
        - 落库后执行期解析器同样按 semantic 解析 → 与示范用例同机制

        Args:
            result (Dict): 定位结果（就地更新 selectors/params/semantic/message）
            step (Dict): 步骤（name/action/params）
            page: Playwright 页面
        """
        action = step.get('action', '')
        name = step.get('name', '') or ''
        order = step.get('order', 0)
        if not self._semantic_enabled():
            return

        semantic = None
        # 1) 模式步骤（"xxx选择：yyy"/"xxx切换为：yyy"）→ 模式语义（text=锚点值 + contains），
        #    内部已做 resolve 校验 + row_checkbox→row 降级
        pattern_result = self._preprocess_step_pattern(name, action)
        if pattern_result and pattern_result['selectors']:
            semantic = self._infer_semantic_from_pattern(step, pattern_result, page)
        # 2) 非模式静态步骤 → 语义解析优先路径（构造候选 semantic → resolve 命中）
        if not semantic:
            sem_result = self._try_semantic_execute(page, step, self._get_page_scope(page))
            if sem_result:
                semantic = sem_result['semantic']

        if not semantic:
            return

        # 解析 semantic → 拿到执行期选择器链，前置到候选（验证/执行都优先走语义链）
        chain: List[str] = []
        try:
            from app.executors.ui_context import ExecutionContext
            scope = semantic.get('scope')
            ctx = ExecutionContext(dict(self.collected_parameters or {}))
            sem_step: Dict[str, Any] = {
                'name': str(semantic.get('text') or ''),
                'action': 'click',
                'params': {},
                'semantic': semantic,
            }
            chain = self._semantic_resolver.resolve_step(page, sem_step, scope, ctx) or []
            chain = [sel for sel in chain if sel]
        except Exception as e:
            logger.debug(f"步骤 {order} 语义解析链获取失败: {e}")

        if chain:
            existing_sel = result['params'].get('selector', '')
            new_cands = [
                {'type': 'css', 'value': sel, 'confidence': 0.95, 'priority': i + 1}
                for i, sel in enumerate(chain)
            ]
            # 原 selector 保留为最后兜底（去重）
            if existing_sel and existing_sel not in chain:
                new_cands.append({
                    'type': 'css', 'value': existing_sel,
                    'confidence': 0.8, 'priority': len(chain) + 1,
                })
            result['selectors']['primary'] = chain[0]
            result['selectors']['candidates'] = new_cands
            result['params']['selector'] = chain[0]

        result['semantic'] = semantic
        result['message'] = f'使用已有选择器 + 语义化升级: {result["params"].get("selector", "")}'
        logger.info(f"步骤 {order} 已有选择器步骤语义化升级: {semantic}")

    def _infer_semantic_from_pattern(self, step: Dict[str, Any], pattern_result: Dict[str, Any], page) -> Optional[Dict[str, Any]]:
        """模式匹配成功后生成 semantic（type 来自模式类型，contains 来自"xxx：值"锚点值）"""
        name = step.get('name', '')
        # 模式步骤（"xxx：值"）的语义文本 = 冒号后的锚点值（如 预定会议/北京展厅会议室），
        # 与示范用例 semantic.text 对齐（tab=标签值、row_checkbox=行值）；
        # 明显动态值 CTX 化，静态值保留原样
        pattern_value = self._extract_pattern_value(name)
        if pattern_value:
            text = self._ctxify_if_dynamic(pattern_value)
        else:
            text = self._extract_semantic_text(name)
        if not text:
            return None
        ptype = pattern_result.get('type', '')
        if ptype == 'list_checkbox':
            stype = 'row_checkbox'  # 列表选择通常命中表格行复选框
        elif ptype == 'tab_item':
            stype = 'tab'
        else:
            stype = 'button'
        semantic: Dict[str, Any] = {'type': stype, 'text': text}
        scope = self._get_page_scope(page)
        if scope:
            semantic['scope'] = scope
        # 模式步骤均为点击类，"xxx：值"锚点值走 contains（动态数据 CTX 化）
        contains = self._extract_contains(name)
        if contains:
            semantic['contains'] = contains
        nth = self._extract_nth(name)
        if nth is not None:
            semantic['nth'] = nth
        # 反向生成校验：生成的 semantic 必须能被执行期解析器解析为真实命中的选择器链，
        # 否则落库后执行必不稳（复制示范用例"语义目标能稳定解析并执行"的成功经验）。
        # row_checkbox → row 降级重试（普通 <tr> 表格行场景，解析器 row 模式含 tr:has-text）
        if not self._validate_semantic_resolves(page, semantic):
            if semantic.get('type') == 'row_checkbox':
                semantic['type'] = 'row'
                logger.info(f"步骤语义校验失败，降级 row_checkbox→row: {semantic}")
            else:
                logger.info(f"步骤语义校验失败，放弃生成 semantic: {semantic}")
                return None
            if not self._validate_semantic_resolves(page, semantic):
                logger.info(f"步骤语义降级后仍校验失败，放弃生成 semantic: {semantic}")
                return None
        return semantic

    def _validate_semantic_resolves(self, page, semantic: Dict[str, Any]) -> bool:
        """
        校验 semantic 能被执行期 resolve_step 解析，且选择器链中至少一条真实命中页面元素
        （执行期 _do_click 会遍历整条链并逐条回退，故任一条命中即视为可执行）
        """
        try:
            from app.executors.ui_context import ExecutionContext
            scope = semantic.get('scope')
            ctx = ExecutionContext(dict(self.collected_parameters or {}))
            step: Dict[str, Any] = {
                'name': str(semantic.get('text') or ''),
                'action': 'click',
                'params': {},
                'semantic': semantic,
            }
            chain = self._semantic_resolver.resolve_step(page, step, scope, ctx) or []
            for sel in chain:
                try:
                    if page.query_selector(sel) is not None:
                        return True
                except Exception:
                    continue
        except Exception as e:
            logger.debug(f"semantic 校验异常: {e}")
        return False

    def _extract_semantic_text(self, name: str) -> Optional[str]:
        """
        从步骤名提取语义键文本
        优先级：1. 【】显式语义键  2. 剔除前导动作动词后的核心词
        """
        text = (name or '').strip()
        if not text:
            return None
        m = re.search(r'【(.+?)】', text)
        if m:
            return m.group(1).strip() or None
        # 截断"xxx：值"动态模式：语义键仅保留锚点部分（值走 contains / CTX）
        text = re.split(r'[:：]', text, 1)[0].strip()
        for v in self._ACTION_VERBS:
            if text.startswith(v):
                text = text[len(v):].strip()
                break
        text = text.strip('，,。 ')
        return text or None

    def _extract_pattern_value(self, name: str) -> Optional[str]:
        """提取"xxx：值"模式中的锚点值（冒号后的内容），无则返回 None"""
        m = re.search(r'[:：]\s*(.+)$', name or '')
        if not m:
            return None
        return m.group(1).strip() or None

    def _ctxify_if_dynamic(self, value: str) -> str:
        """仅对明显动态值 CTX 化（与 _ctf_param 判定一致），静态值保留原样"""
        if re.search(r'[A-Za-z0-9@._\-]', value) or len(value) >= 6:
            return self._ctxify(value)
        return value

    def _extract_contains(self, name: str) -> Optional[str]:
        """步骤名"xxx：值"模式中的锚点值（动态数据 CTX 化，静态值保留原样）"""
        value = self._extract_pattern_value(name)
        if not value:
            return None
        return self._ctxify_if_dynamic(value)

    def _extract_nth(self, name: str) -> Optional[int]:
        """步骤名含"第 N 个"时提取（否则不生成）"""
        m = re.search(r'第\s*([0-9一二三四五六七八九十]+)\s*个', name)
        if not m:
            return None
        text = m.group(1)
        if text.isdigit():
            return int(text)
        cjk = '零一二三四五六七八九'
        if text in cjk:
            return cjk.index(text)
        return None

    def _ctxify(self, value: str) -> str:
        """
        动态值 CTX 化：写入 collected_parameters 初值，返回 {__CTX:key__} 占位
        同值复用同一 key，保证创建/断言/清理同名
        """
        if value in self._ctx_value_map:
            return f"{{__CTX:{self._ctx_value_map[value]}__}}"
        key = f"param{len(self.collected_parameters) + 1}"
        self._ctx_value_map[value] = key
        self.collected_parameters[key] = value
        logger.info(f"动态值 CTX 化: '{value}' -> {{__CTX:{key}__}}")
        return f"{{__CTX:{key}__}}"

    def _try_semantic_execute(self, page, step: Dict[str, Any], scope: Optional[str]) -> Optional[Dict[str, Any]]:
        """
        语义解析优先：构造候选 semantic → resolve_step 命中选择器链
        实际执行验证由 locate_steps 的统一 _execute_step 完成（与 keyword 路径一致）
        成功 = 该 semantic 目标已通过执行期解析器验证，落库后执行必稳
        """
        if not self._semantic_enabled():
            return None
        name = step.get('name', '')
        # 仅静态步骤（无"xxx：值"动态值）走语义解析优先，避免语义键被值污染
        if re.search(r'[:：]', name):
            return None
        semantic = self._build_candidate_semantic(step, scope)
        if not semantic:
            return None
        try:
            from app.executors.ui_context import ExecutionContext
            ctx = ExecutionContext()
            # 候选 semantic 注入步骤，供解析器消费
            candidate_step = dict(step)
            candidate_step['semantic'] = semantic
            chain = self._semantic_resolver.resolve_step(page, candidate_step, scope, ctx) or []
            if not chain:
                return None
            # 关键保证：链首选择器必须真实命中页面元素，否则该 semantic 未经执行期验证，
            # 直接返回会造成"假命中"（如 type=button 但目标实为 DIV → button:has-text 无元素，
            # 后续 _execute_step 必失败）。未命中则回退 keyword 路径，由反向生成产出 semantic。
            try:
                el = page.query_selector(chain[0])
            except Exception as e:
                logger.debug(f"语义链首校验异常: {e}")
                el = None
            if el is None:
                logger.info(f"步骤 {step.get('order')} 语义链首未命中页面元素，回退 keyword 定位")
                return None
            # 用解析链定位真实元素，反推细化 semantic.type（结构化元素纠正 button→checkbox/tab 等）
            try:
                refined_type = self._infer_element_type(step.get('action', ''), el)
                if refined_type != semantic.get('type'):
                    logger.info(f"步骤 {step.get('order')} 语义类型细化: "
                                f"{semantic.get('type')} -> {refined_type}")
                    semantic['type'] = refined_type
            except Exception as e:
                logger.debug(f"语义类型细化失败（非致命）: {e}")
            return {'chain': chain, 'semantic': semantic}
        except Exception as e:
            logger.debug(f"语义解析优先失败: {e}")
            return None

    def _infer_semantic_from_element(self, step: Dict[str, Any], element, page) -> Optional[Dict[str, Any]]:
        """
        keyword 定位成功后反向生成 semantic 目标
        从步骤名 + 候选元素 tag/class/role + 父容器 + 运行时 scope 推断
        """
        name = step.get('name', '')
        action = step.get('action', '')
        text = self._extract_semantic_text(name)
        if not text:
            return None
        semantic: Dict[str, Any] = {
            'type': self._infer_element_type(action, element),
            'text': text,
        }
        scope = self._get_page_scope(page)
        if scope:
            semantic['scope'] = scope
        # contains：仅点击类步骤"xxx：值"模式的锚点值（动态数据 CTX 化）
        # fill/select 值不参与选择器过滤（输入框按占位符定位），值走 _ctf_param 收集
        if action == 'click':
            contains = self._extract_contains(name)
            if contains:
                semantic['contains'] = contains
        # container：候选元素父容器
        container = self._infer_container(element)
        if container and container.get('selector'):
            semantic['container'] = container
        # nth：步骤名"第 N 个"
        nth = self._extract_nth(name)
        if nth is not None:
            semantic['nth'] = nth
        return semantic

    def _infer_element_type(self, action: str, element) -> str:
        """推断 semantic.type（action 映射 + 候选元素 tag/class/role 反推）"""
        if action in ('fill', 'input', 'type'):
            tag = self._safe_attr(element, 'tagName')
            if tag and tag.lower() == 'textarea':
                return 'textarea'
            return 'input'
        if action in ('select', 'select_option'):
            return 'select'
        if element is None:
            return 'button'
        tag = (self._safe_attr(element, 'tagName') or '').lower()
        cls = self._safe_attr(element, 'class') or ''
        role = self._safe_attr(element, 'role') or ''
        # 强结构模式优先
        if 'checkbox' in cls or (tag == 'input' and self._safe_attr(element, 'type') == 'checkbox'):
            ctype = (self._infer_container(element) or {}).get('type', '')
            return 'row_checkbox' if ctype == 'table' else 'checkbox'
        if 'el-tabs__item' in cls or role == 'tab':
            return 'tab'
        if 'el-table__row' in cls or tag == 'tr':
            return 'row'
        # 其余：默认 button（点击语义）
        return 'button'

    def _safe_attr(self, element, attr: str, default: str = '') -> str:
        """安全读取元素属性（元素可能已被 Playwright 释放）"""
        if element is None:
            return default
        try:
            if attr == 'tagName':
                val = element.evaluate("el => el.tagName")
            else:
                val = element.get_attribute(attr)
            return val if val is not None else default
        except Exception:
            return default

    def _infer_container(self, element) -> Optional[Dict[str, Any]]:
        """推断父容器（复用 keyword_matcher._detect_parent_container）"""
        if element is None:
            return None
        try:
            return _detect_parent_container(element)
        except Exception as e:
            logger.debug(f"父容器推断失败: {e}")
            return None

    def _detect_verify_after(self, step: Dict[str, Any], page) -> Optional[Dict[str, Any]]:
        """
        条件 verify：点击后观察到确定状态变化才生成（保守策略）
        观察不到确定状态变化则不生成——避免错误断言导致执行失败
        """
        if step.get('action') != 'click' or page is None:
            return None
        name = step.get('name', '')
        try:
            # 1. 成功提示文案
            msg = page.locator('.el-message--success, .el-message.success, .el-notification--success')
            if msg.count() > 0 and msg.first.is_visible(timeout=1200):
                text = msg.first.inner_text().strip()
                if text:
                    return {'type': 'text_visible', 'text': text}
        except Exception:
            pass
        # 2. 弹窗/抽屉/确认框（步骤名含弹窗关键词时）
        dialog_keywords = ['新建', '编辑', '添加', '新增', '创建', '修改', '详情',
                           '确定创建', '取消会议', '删除', '选择会议室']
        if any(kw in name for kw in dialog_keywords):
            try:
                for sel in ['.el-dialog', '.el-drawer', '.el-message-box']:
                    loc = page.locator(sel)
                    if loc.count() > 0 and loc.first.is_visible(timeout=1200):
                        return {'type': 'dialog'}
            except Exception:
                pass
        return None

    def _ctf_param(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """
        fill 步骤动态值 CTX 化（仅对明显动态值生效，静态值保留原样）
        value → {__CTX:key__}，初值收集进 collected_parameters
        """
        value = params.get('value')
        if not isinstance(value, str) or len(value) < 4:
            return params
        # 明显动态值：含字母/数字/@ 等，或较长中文（≥6）
        if re.search(r'[A-Za-z0-9@._\-]', value) or len(value) >= 6:
            params['value'] = self._ctxify(value)
        return params

    def _capture_screenshot(self, page, order: int, result: Dict[str, Any]) -> None:
        """截图（复用现有截图逻辑，非致命）"""
        try:
            screenshot_path = os.path.join(
                self.screenshot_dir,
                f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
            )
            page.screenshot(path=screenshot_path)
            with open(screenshot_path, 'rb') as f:
                result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
        except Exception as e:
            logger.debug(f"截图失败（非致命）: {e}")

    def _preprocess_step_pattern(self, step_name: str, action: str) -> Optional[Dict[str, Any]]:
        """
        预处理特殊步骤模式，生成选择器

        处理模式：
        1. "xxx选择：yyy" → 列表内复选框（如"会议室选择：北京展厅会议室"）
        2. "xxx切换为：yyy" / "切换到：yyy" → Tab切换项（如"时间切换为：预定会议"）

        Args:
            step_name (str): 步骤名称
            action (str): 动作类型

        Returns:
            Optional[Dict]: 模式匹配结果，包含：
                - type: 模式类型
                - selectors: 候选选择器列表
                - confidence: 置信度

        Example:
            >>> _preprocess_step_pattern("会议室选择：北京展厅会议室", "click")
            {
                'type': 'list_checkbox',
                'selectors': [
                    '.el-checkbox:has-text("北京展厅会议室")',
                    '.el-table__row:has-text("北京展厅会议室") .el-checkbox'
                ],
                'confidence': 0.85
            }
        """
        import re

        if action != 'click':
            return None

        # 模式1：列表选择模式（"xxx选择：yyy"）
        # 适用于：会议室选择、设备选择、人员选择等
        match = re.search(r'选择[:：]\s*(.+)$', step_name)
        if match:
            item_name = match.group(1).strip()
            logger.info(f"识别列表选择模式，目标项: {item_name}")

            return {
                'type': 'list_checkbox',
                'selectors': [
                    f'.el-checkbox:has-text("{item_name}")',
                    f'.el-table__row:has-text("{item_name}") .el-checkbox',
                    f'.el-tree-node:has-text("{item_name}") .el-checkbox',
                    f'li:has-text("{item_name}") .el-checkbox',
                    f'[class*="list"]:has-text("{item_name}") .el-checkbox',
                    # 普通 <tr> 表格行（会议室等列表常为原生表格，勾选通过点击行触发）
                    f'tr:has-text("{item_name}")',
                ],
                'confidence': 0.85
            }

        # 模式2：Tab切换模式（"xxx切换为：yyy" / "切换到：yyy"）
        match = re.search(r'切换[为到]?[:：]\s*(.+)$', step_name)
        if match:
            tab_name = match.group(1).strip()
            logger.info(f"识别Tab切换模式，目标Tab: {tab_name}")

            return {
                'type': 'tab_item',
                'selectors': [
                    f'.el-tabs__item:has-text("{tab_name}")',
                    f'[role="tab"]:has-text("{tab_name}")',
                    f'.tab-item:has-text("{tab_name}")',
                    f'li[role="tab"]:has-text("{tab_name}")',
                    # 自定义 tab 元素（非 el-tabs 结构）兜底：精确文本匹配
                    f'text="{tab_name}"',
                ],
                'confidence': 0.85
            }

        return None

    def _is_login_step(self, step: Dict[str, Any]) -> bool:
        """
        判断步骤是否为登录流程步骤（auto_login 已自动登录时应跳过）

        覆盖两类：
        1. navigate 到登录页/目标页（selector 或 url 是 http 链接）
        2. 登录表单操作（输入账号/密码/验证码、勾选协议、点击登录）
        """
        name = step.get('name', '') or ''
        action = step.get('action', '') or ''
        params = step.get('params', {}) or {}

        # 1. navigate 到 http 地址（登录页/目标页）——URL 不能当 CSS 校验，且 auto_login 已直达
        if action == 'navigate':
            sel = str(params.get('selector', '') or '')
            url = str(params.get('url', '') or '')
            if 'http' in sel or 'http' in url:
                return True

        # 2. 登录表单操作步骤（排除功能中心/首页等菜单导航，避免误跳过业务步骤）
        login_kw = ('用户名', '账号', '手机号', '密码', '验证码', '协议', '勾选', '登录')
        if any(kw in name for kw in login_kw):
            if '功能中心' in name or '首页' in name or '菜单' in name:
                return False
            return True

        return False

    def _get_login_selector(self, step_name: str) -> Optional[str]:
        """
        获取登录相关步骤和导航步骤的选择器（使用已验证的模板）

        Args:
            step_name (str): 步骤名称

        Returns:
            Optional[str]: 选择器，如果不是已知步骤返回 None
        """
        # 登录相关关键词映射到选择器
        login_selector_map = {
            # 输入账号
            '账号': 'input[placeholder*="手机号"]',
            '用户名': 'input[placeholder*="手机号"]',
            '手机号': 'input[placeholder*="手机号"]',
            # 输入密码
            '密码': 'input[type="password"]',
            # 输入验证码
            '验证码': 'input[placeholder*="图"]',
            # 勾选协议
            '协议': '.el-checkbox',
            '勾选': '.el-checkbox',
            # 点击登录
            '登录': 'button:has-text("登录")',
        }

        # 检查步骤名称是否包含登录关键词
        for keyword, selector in login_selector_map.items():
            if keyword in step_name:
                return selector

        # ⚠️ 新增：功能中心导航相关步骤
        nav_selector_map = {
            '功能中心': '//*[@id="Home"]/div[1]/div[1]',
            '首页': '//*[@id="Home"]/div[1]/div[1]',
        }

        for keyword, selector in nav_selector_map.items():
            if keyword in step_name:
                return selector

        return None


# ==================== 工厂函数 ====================

def get_smart_locate_service() -> SmartLocateService:
    """
    获取智能定位服务实例

    Returns:
        SmartLocateService: 服务实例
    """
    return SmartLocateService()