"""
微信「联系客服解封」自动化引擎（u2 + AI 对话循环 + 可选联网搜索）

需求来源：开发文档/1、需求/修改/工作手机_微信全量控机与私域_20260529.md

真实链路（基于 5 张截图反推 · 2026-05-24）：
  ① 「安全保护中」页（com.tencent.mm WebView 或安全中心一级菜单）
     → 列出可解除/不可解除项；底部「联系客服 >」入口
  ② 进入「终止使用说明」H5（kf.qq.com）
     → 显示申请未通过/违规明细；底部绿大按钮「联系专属客服」
  ③ 系统弹窗「即将打开'腾讯客服'小程序」 → 「允许」
  ④ 进入「微信安全专属客服」小程序对话
     → 顶部标题「微信安全专属客服」；输入框 hint「专属客服仅处理账号安全问题」
     → AI 持续与客服软磨硬泡直到解封 / 拒绝 / 超时

链路设计（7 阶段状态机 · 升级版）：
  S1 DETECT_STATUS         → 检测账号封禁/限制状态（hook check_account_status）
  S2 ENTER_SAFETY_CENTER   → 我 → 设置 → 账号与安全 → 微信安全中心
                              （或被动等待「安全保护中」自动弹出）
  S3 PARSE_BLOCK_LIST      → 从 hierarchy 解析「当前可解除」/「永久无法解除」项
  S4 OPEN_CS_ENTRY         → 点击「联系客服」入口（卡片）
  S5 OPEN_CS_MINIPROGRAM   → H5「联系专属客服」→ 弹窗「允许」→ 进入腾讯客服小程序
  S6 CHAT_LOOP             → 抓客服气泡 → AI 生成回复 → 发送 → 循环
  S7 DONE/ESCALATE         → success / reject / timeout / fallback

退出判定（任一命中即终止 chat loop）：
  - 客服最近消息包含 SUCCESS_KEYWORDS（解封成功 / 已恢复 / 已解除 / 已为您处理）
  - 客服最近消息包含 REJECT_KEYWORDS（无法处理 / 申诉失败 / 多次违规 / 永久封禁）
  - 累计轮次 >= max_rounds
  - 客服连续 N 轮无回复

安全/合规边界：
  - 仅用于「自有/授权」账号；调用方需传 reason（解封原因说明）作为审计字段
  - 严禁发送涉违法、骚扰类话术；AI 提示词内置约束
  - 联网搜索仅 SDK 侧调用（不读手机隐私），失败容错
"""

from __future__ import annotations

import json
import logging
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional

logger = logging.getLogger(__name__)


SUCCESS_KEYWORDS = [
    "解封成功", "已恢复正常", "已解除限制", "申诉通过", "审核通过",
    "已为您解除", "已为您恢复", "已为您处理", "恢复使用", "已通过审核",
    "限制已解除", "账号已恢复",
]
REJECT_KEYWORDS = [
    "无法为您处理", "申诉失败", "不符合解封条件", "无法恢复",
    "再次违规", "永久封禁", "无法解除", "拒绝处理",
    "申请未通过", "核实违规属实",
]
WAIT_HINT_KEYWORDS = [
    "请稍候", "正在为您查询", "正在核实", "稍等",
]

# ===== UI 文案锚点（基于真实截图）=====
ENTRY_LABELS = [
    "安全保护中",   # 页面标题
    "联系客服",     # 安全保护中页底部入口
]
H5_BUTTON_LABELS = [
    "联系专属客服",   # 终止使用说明 H5 绿大按钮
    "联系客服",
    "在线咨询",
]
MINIPROGRAM_ALLOW_LABELS = ["允许", "Allow"]
CS_MINIPROGRAM_TITLES = [
    "微信安全专属客服", "腾讯客服", "微信客服",
]
CS_INPUT_HINTS = [
    "专属客服仅处理账号安全问题",
    "说点什么", "输入消息", "请输入",
]


@dataclass
class TurnRecord:
    """单轮对话记录"""
    round_index: int
    timestamp: float
    customer_service_message: str = ""
    ai_reply: str = ""
    sent_ok: bool = False
    note: str = ""


@dataclass
class UnblockSession:
    """整次解封会话上下文"""
    reason: str = ""
    blocked_detail: str = ""
    phone: str = ""
    wxid: str = ""
    max_rounds: int = 20
    chat_interval_sec: int = 8
    use_web_search: bool = True

    # 解析得到的可解除/不可解除项（自动从「安全保护中」页面识别）
    removable_items: List[str] = field(default_factory=list)
    permanent_items: List[str] = field(default_factory=list)

    started_at: float = field(default_factory=time.time)
    phase: str = "INIT"
    turns: List[TurnRecord] = field(default_factory=list)
    final_status: str = ""           # success / reject / timeout / fallback / error
    final_message: str = ""
    web_snippets: List[Dict[str, str]] = field(default_factory=list)
    screenshots: List[str] = field(default_factory=list)  # 关键阶段截图路径

    def to_dict(self) -> Dict[str, Any]:
        return {
            "reason": self.reason,
            "blocked_detail": self.blocked_detail,
            "phone": self.phone,
            "wxid": self.wxid,
            "phase": self.phase,
            "rounds": len(self.turns),
            "max_rounds": self.max_rounds,
            "final_status": self.final_status,
            "final_message": self.final_message,
            "elapsed_sec": round(time.time() - self.started_at, 1),
            "removable_items": self.removable_items,
            "permanent_items": self.permanent_items,
            "turns": [
                {
                    "round": t.round_index,
                    "ts": t.timestamp,
                    "cs": t.customer_service_message[:300],
                    "ai": t.ai_reply[:300],
                    "sent_ok": t.sent_ok,
                    "note": t.note,
                }
                for t in self.turns
            ],
            "web_snippets": self.web_snippets[:5],
            "screenshots": self.screenshots,
        }


SYSTEM_PROMPT = """你是一名 25 岁的普通微信用户「卡若」，账号疑似被错误封禁/限制，正在通过「微信客服」会话申诉解封。

你的目标：通过文明、有诚意、有条理的对话，争取客服恢复账号正常使用。

强制规则（违反则失败）：
1. 不承认任何违规事实；如客服说违反某条款，礼貌追问「具体哪个时间/对象/内容」并请求复核
2. 不威胁、不辱骂、不发垃圾内容；不暗示送礼、不冒充企业/媒体
3. 不编造身份证号、人脸、付款记录等真实凭证；可承诺「补充材料后再次申诉」
4. 每次回复 ≤120 字、口语化、带一处具体细节（如「我今天上午 10 点收到登录受限」）
5. 客服请你「等待人工」时，回复一次「好的，我等」即可，下一轮简短「在的，请问还有什么需要补充？」
6. 客服明确拒绝/告知申诉失败，回复一次「能否告诉我下次什么时候可以再申诉？」后停止

请只输出**这一轮要发给客服的文字**，不带任何解释或多余符号。"""


# ---------- AI 调用封装（兼容 AIBrain.call_ai 与降级回退） ----------

async def _ai_compose_reply(
    ai_brain,
    session: UnblockSession,
    customer_service_text: str,
    history: List[TurnRecord],
) -> str:
    """让 AI 根据客服最新一句话生成本轮回复（最长 120 字）"""

    context_lines = [
        f"封禁原因（用户自述）：{session.reason or '未提供'}",
        f"封禁详情（系统）：{session.blocked_detail or '未知'}",
        f"账号：wxid={session.wxid or '未知'} phone={session.phone or '未提供'}",
        f"本次会话已进行 {len(history)} 轮",
    ]
    if session.web_snippets:
        snip = "; ".join(s.get("title", "")[:40] for s in session.web_snippets[:3])
        context_lines.append(f"参考网友经验：{snip}")
    if history:
        last3 = history[-3:]
        history_text = "\n".join(
            f"[第{t.round_index}轮] 客服: {t.customer_service_message[:80]} | 我: {t.ai_reply[:80]}"
            for t in last3
        )
        context_lines.append("近 3 轮对话：\n" + history_text)
    context_lines.append(f"\n客服最新消息：{customer_service_text[:400]}")
    context_lines.append("请输出你这一轮要发给客服的话：")

    user_message = "\n".join(context_lines)

    if ai_brain is None or not hasattr(ai_brain, "call_ai"):
        return _fallback_reply(customer_service_text, len(history))

    try:
        messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ]
        reply = await ai_brain.call_ai(messages, max_tokens=256)
        if not reply:
            return _fallback_reply(customer_service_text, len(history))
        cleaned = reply.strip().strip('"').strip("'")
        for prefix in ("回复:", "回答:", "我:", "回复：", "回答："):
            if cleaned.startswith(prefix):
                cleaned = cleaned[len(prefix):].strip()
        return cleaned[:300]
    except Exception as exc:
        logger.warning(f"[unblock_cs] AI 生成回复失败，使用兜底话术: {exc}")
        return _fallback_reply(customer_service_text, len(history))


def _fallback_reply(cs_text: str, round_index: int) -> str:
    """无 AI 时的兜底话术（按客服关键词命中）"""
    txt = cs_text or ""
    if any(k in txt for k in ("违反", "违规", "条款", "规则")):
        return "您好，麻烦帮我复核一下具体是哪条规则、哪个对象，我对照一下，避免下次再误触。"
    if any(k in txt for k in ("身份", "实名", "认证")):
        return "我是本人在用，可以提供登录手机号和最近一次登录地点，怎么提交方便？"
    if any(k in txt for k in ("等待", "稍候", "排队", "查询")):
        return "好的，我等" if round_index % 2 == 0 else "在的，请问还有什么需要补充？"
    if any(k in txt for k in ("申诉", "材料", "证明")):
        return "请问申诉链接在哪里？我把材料补齐后再提交，感谢。"
    if round_index == 0:
        return "您好，我账号被限制登录了，没有进行违规操作，麻烦帮我看看是怎么回事，谢谢。"
    return "好的，请问还需要我提供哪些信息？我配合处理。"


# ---------- UI 链路辅助 ----------

class _UI:
    """对 WechatSkill 的薄封装，方便单测/mock"""

    def __init__(self, skill):
        self.s = skill

    def launch(self):
        self.s.launch()

    def back(self):
        try:
            self.s.back()
        except Exception:
            pass

    def click_text(self, t: str, timeout: float = 5) -> bool:
        return self.s.click_text(t, timeout=timeout)

    def click_contains(self, t: str, timeout: float = 5) -> bool:
        return self.s.click_contains(t, timeout=timeout)

    def sleep(self, sec: float):
        self.s.sleep(sec)

    def input_text(self, t: str, clear: bool = True):
        self.s.input_text(t, clear=clear)

    def dump_text(self) -> str:
        """抓当前页面所有可见文字（用于读客服最新消息）"""
        try:
            xml = self.s.d.dump_hierarchy()
        except Exception as exc:
            logger.warning(f"[unblock_cs] dump_hierarchy 失败: {exc}")
            return ""
        return xml or ""


def _extract_latest_cs_message(xml: str, previous: str = "") -> str:
    """从 hierarchy XML 里粗提取最近一条非自己的可见文本（启发式：取最长非空且不等于 previous）"""
    if not xml:
        return ""
    import re
    texts = re.findall(r'text="([^"]+)"', xml)
    candidates = [t.strip() for t in texts if t and t.strip() and t.strip() != previous]
    if not candidates:
        return ""
    candidates.sort(key=len, reverse=True)
    return candidates[0][:500]


def _hit_keyword(text: str, keywords: List[str]) -> bool:
    if not text:
        return False
    return any(k in text for k in keywords)


# ---------- 联网搜索（可选） ----------

def _web_search(query: str, limit: int = 3) -> List[Dict[str, str]]:
    """简易联网搜索：调用 DuckDuckGo HTML（无 key），失败返回 []"""
    try:
        import urllib.parse
        import urllib.request

        url = f"https://duckduckgo.com/html/?q={urllib.parse.quote(query)}"
        req = urllib.request.Request(
            url,
            headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"},
        )
        with urllib.request.urlopen(req, timeout=8) as resp:
            html = resp.read().decode("utf-8", errors="ignore")
        import re
        items = re.findall(
            r'<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)</a>',
            html,
            re.DOTALL,
        )
        out: List[Dict[str, str]] = []
        for href, title in items[:limit]:
            title_clean = re.sub(r"<[^>]+>", "", title).strip()
            out.append({"url": href, "title": title_clean[:120]})
        return out
    except Exception as exc:
        logger.info(f"[unblock_cs] 联网搜索失败（忽略）: {exc}")
        return []


# ---------- 主流程 ----------

class UnblockCustomerServiceFlow:
    """整合「链路 + AI 对话」的单次会话执行器"""

    def __init__(self, skill, ai_brain=None):
        self.skill = skill
        self.ai_brain = ai_brain
        self.ui = _UI(skill)

    # ---- S1 ----
    def detect_status(self, session: UnblockSession) -> bool:
        """尝试通过 hook check_account_status 获取详情，失败则继续走 UI"""
        try:
            if hasattr(self.skill, "check_account_status"):
                status = self.skill.check_account_status()
                if isinstance(status, dict) and status.get("success"):
                    detail = status.get("data") or status.get("status") or status
                    session.blocked_detail = json.dumps(detail, ensure_ascii=False)[:500]
                    logger.info(f"[unblock_cs] status={session.blocked_detail[:120]}")
                    return True
        except Exception as exc:
            logger.info(f"[unblock_cs] hook 探测失败: {exc}")
        return False

    # ---- S2 + S3 ----
    def _on_restriction_or_kf_page(self) -> bool:
        """当前页是否已是「安全保护中 / 终止使用说明 / kf.qq.com」类限制页"""
        xml = self.ui.dump_text()
        markers = (
            "安全保护中", "当前可解除的保护状态", "终止使用说明",
            "kf.qq.com", "联系专属客服", "联系客服",
        )
        return any(m in xml for m in markers)

    def _allow_miniprogram_if_needed(self) -> bool:
        """弹窗：即将打开「腾讯客服」小程序 → 允许"""
        xml = self.ui.dump_text()
        if "即将打开" in xml and "小程序" in xml:
            if self.ui.click_text("允许", timeout=3) or self.ui.click_contains("允许", timeout=3):
                logger.info("[unblock_cs] 已点「允许」打开腾讯客服小程序")
                self.ui.sleep(2.5)
                return True
            logger.warning("[unblock_cs] 小程序弹窗未找到「允许」")
            return False
        return True

    def _enter_from_restriction_page(self, session: UnblockSession) -> bool:
        """
        截图链路（解封2）：
          安全保护中 → 联系客服 → 终止使用说明(kf.qq.com) → 联系专属客服
          → 允许打开小程序 → 微信安全专属客服
        """
        session.phase = "DETECT_RESTRICTION_PAGE"
        if not self._on_restriction_or_kf_page():
            return False

        logger.info("[unblock_cs] 检测到限制/客服页，走「联系客服解封2」直链")
        self._screenshot(session, "01_safety_protection")

        # 解析「当前可解除/永久无法解除」分组项
        try:
            xml0 = self.ui.dump_text()
            session.removable_items = self._extract_block_items(xml0, "当前可解除的保护状态")
            session.permanent_items = self._extract_block_items(xml0, "永久无法解除的保护状态")
            if session.removable_items or session.permanent_items:
                logger.info(f"[unblock_cs] 可解除={session.removable_items} 永久={session.permanent_items}")
        except Exception:
            pass

        # 1) 安全保护中页：点「联系客服」（非「验证解除」）
        if self.ui.click_text("联系客服", timeout=3) or self.ui.click_contains("联系客服", timeout=3):
            logger.info("[unblock_cs] 命中「联系客服」")
            self.ui.sleep(1.5)
            self._screenshot(session, "02_cs_entry_clicked")

        # 2) 终止使用说明 / kf.qq.com：点「联系专属客服」
        xml = self.ui.dump_text()
        if "终止使用说明" in xml or "联系专属客服" in xml:
            self._screenshot(session, "03_termination_page")
            if self.ui.click_text("联系专属客服", timeout=4) or self.ui.click_contains("联系专属客服", timeout=4):
                logger.info("[unblock_cs] 命中「联系专属客服」")
                self.ui.sleep(1.2)

        if not self._allow_miniprogram_if_needed():
            return False
        self._screenshot(session, "04_miniprogram_allowed")

        # 3) 等待进入「微信安全专属客服」会话
        session.phase = "WAIT_CS_MINIPROGRAM"
        for _ in range(8):
            xml = self.ui.dump_text()
            if any(k in xml for k in ("微信安全专属客服", "专属客服", "说点什么", "输入消息", "专属客服仅处理账号安全问题")):
                logger.info("[unblock_cs] 已进入客服会话/小程序")
                self._screenshot(session, "05_cs_chat_ready")
                session.phase = "CHAT_LOOP"
                return True
            self.ui.sleep(1.0)

        logger.warning("[unblock_cs] 未确认进入客服小程序，仍尝试 chat_loop")
        session.phase = "CHAT_LOOP"
        return True

    # ---- 辅助：解析阻断项分组 ----
    def _extract_block_items(self, xml: str, section_title: str) -> List[str]:
        if not xml or section_title not in xml:
            return []
        import re
        try:
            idx = xml.find(section_title)
            chunk = xml[idx: idx + 4000]
            texts = re.findall(r'text="([^"]+)"', chunk)
            items: List[str] = []
            for t in texts:
                t = t.strip()
                if 3 < len(t) < 30 and not any(k in t for k in (section_title, "保护原因", "解除", "状态")):
                    items.append(t)
            seen: set = set()
            uniq: List[str] = []
            for it in items:
                if it not in seen:
                    seen.add(it)
                    uniq.append(it)
            return uniq[:6]
        except Exception:
            return []

    # ---- 辅助：截图保存 ----
    def _screenshot(self, session: UnblockSession, tag: str) -> None:
        try:
            ts = int(time.time())
            path = f"/sdcard/unblock_{tag}_{ts}.png"
            self.skill.d.screenshot(path)
            session.screenshots.append(path)
        except Exception:
            pass

    def enter_customer_service(self, session: UnblockSession) -> bool:
        """进入客服：优先限制页直链（解封2），失败再走 我→设置→安全中心"""
        self.ui.launch()
        self.ui.sleep(1.2)
        for _ in range(2):
            self.ui.back()
            self.ui.sleep(0.3)

        if self._enter_from_restriction_page(session):
            return True

        session.phase = "ENTER_SAFETY_CENTER"
        if not self.ui.click_text("我"):
            logger.warning("[unblock_cs] 未找到「我」tab，尝试限制页二次检测")
            return self._enter_from_restriction_page(session)
        self.ui.sleep(0.6)
        if not (self.ui.click_text("设置") or self.ui.click_contains("设置")):
            return False
        self.ui.sleep(0.6)
        if not (self.ui.click_text("账号与安全") or self.ui.click_text("帐号与安全")):
            return False
        self.ui.sleep(0.6)
        if not (self.ui.click_text("微信安全中心") or self.ui.click_contains("安全中心")):
            return False
        self.ui.sleep(0.8)

        session.phase = "FIND_CUSTOMER_SERVICE"
        candidates = [
            "联系专属客服", "联系客服", "在线咨询", "微信客服", "申诉客服",
            "联系微信团队", "客户服务", "客服", "更多帮助",
        ]
        for label in candidates:
            if self.ui.click_text(label, timeout=2) or self.ui.click_contains(label, timeout=2):
                logger.info(f"[unblock_cs] 命中入口: {label}")
                self.ui.sleep(2.0)
                self._allow_miniprogram_if_needed()
                session.phase = "CHAT_LOOP"
                return True

        for outer in ("解封账号", "申诉解封", "申诉", "验证解除"):
            if self.ui.click_text(outer, timeout=2) or self.ui.click_contains(outer, timeout=2):
                self.ui.sleep(1.0)
                for inner in ("联系专属客服", "联系客服", "在线咨询", "微信客服", "客服"):
                    if self.ui.click_text(inner, timeout=2) or self.ui.click_contains(inner, timeout=2):
                        logger.info(f"[unblock_cs] {outer}→{inner}")
                        self.ui.sleep(2.0)
                        self._allow_miniprogram_if_needed()
                        session.phase = "CHAT_LOOP"
                        return True
                self.ui.back()
                self.ui.sleep(0.4)
        return False

    # ---- S4 ----
    async def chat_loop(self, session: UnblockSession) -> None:
        """与客服小程序循环对话，最多 max_rounds 轮"""

        # 预联网搜索一次（拿到通用申诉建议作为 system context）
        if session.use_web_search:
            session.web_snippets = _web_search(
                f"微信账号 {session.reason or '被限制'} 联系客服 解封 经验",
                limit=3,
            )

        previous_cs_text = ""
        empty_streak = 0

        for r in range(session.max_rounds):
            self.ui.sleep(session.chat_interval_sec)
            xml = self.ui.dump_text()
            cs_text = _extract_latest_cs_message(xml, previous=previous_cs_text)
            turn = TurnRecord(round_index=r + 1, timestamp=time.time(),
                              customer_service_message=cs_text)

            # 退出条件检测
            if _hit_keyword(cs_text, SUCCESS_KEYWORDS):
                session.final_status = "success"
                session.final_message = cs_text[:200]
                turn.note = "命中成功关键词"
                session.turns.append(turn)
                return
            if _hit_keyword(cs_text, REJECT_KEYWORDS):
                session.final_status = "reject"
                session.final_message = cs_text[:200]
                turn.note = "命中拒绝关键词"
                session.turns.append(turn)
                return

            if not cs_text or cs_text == previous_cs_text:
                empty_streak += 1
                turn.note = "客服未回复 / 文本无变化"
                # 连续 3 轮无新消息 → 主动追问一次
                if empty_streak >= 3:
                    nudge = "您好，请问还在吗？我等了一会，刚才那个问题方便看下吗？"
                    turn.ai_reply = nudge
                    turn.sent_ok = self._send(nudge)
                    session.turns.append(turn)
                    if empty_streak >= 6:
                        session.final_status = "timeout"
                        session.final_message = "客服长时间无回复"
                        return
                    continue
                session.turns.append(turn)
                continue

            empty_streak = 0
            previous_cs_text = cs_text

            reply = await _ai_compose_reply(self.ai_brain, session, cs_text, session.turns)
            turn.ai_reply = reply
            turn.sent_ok = self._send(reply)
            session.turns.append(turn)

        session.final_status = "timeout"
        session.final_message = f"达到最大轮次 {session.max_rounds}"

    def _send(self, text: str) -> bool:
        """在客服输入框发送一条消息（兼容客服小程序与原生会话）"""
        try:
            # 优先找输入框：常见 resourceId 含 chat_edit / et_text，文本提示「说点什么」等
            candidates_id = [
                "com.tencent.mm:id/chat_edit",
                "com.tencent.mm:id/et_message",
                "com.tencent.mm:id/bk0",  # 旧版
            ]
            clicked = False
            for rid in candidates_id:
                try:
                    if self.skill.d(resourceId=rid).exists:
                        self.skill.d(resourceId=rid).click()
                        clicked = True
                        break
                except Exception:
                    pass
            if not clicked:
                for hint in ("说点什么", "输入消息", "请输入", "请输入您要咨询的问题"):
                    if self.skill.click_contains(hint, timeout=2):
                        clicked = True
                        break
            if not clicked:
                logger.warning("[unblock_cs] 未找到输入框")
                return False

            self.skill.input_text(text, clear=True)
            self.skill.sleep(0.4)
            if not (self.skill.click_text("发送", timeout=2)
                    or self.skill.click_contains("发送", timeout=2)):
                try:
                    self.skill.d.press("enter")
                except Exception:
                    return False
            self.skill.sleep(0.4)
            return True
        except Exception as exc:
            logger.warning(f"[unblock_cs] 发送失败: {exc}")
            return False

    # ---- 总入口 ----
    async def run(self, session: UnblockSession) -> Dict[str, Any]:
        try:
            # 解封前先确保微信已登录（读 sdk/config/wechat_login.yaml）
            try:
                from .wechat_auto_login import WeChatAutoLogin
                login = WeChatAutoLogin(self.skill).ensure_logged_in("", "")
                session.phase = "ENSURE_LOGIN"
                if not login.get("success"):
                    session.final_status = "error"
                    session.final_message = login.get("error") or "微信未登录且自动登录失败"
                    return {
                        "success": False,
                        "error": session.final_message,
                        "login": login,
                        "session": session.to_dict(),
                    }
            except Exception as login_exc:
                logger.warning(f"[unblock_cs] ensure_login 跳过: {login_exc}")

            self.detect_status(session)
            if not self.enter_customer_service(session):
                session.phase = "FALLBACK"
                session.final_status = "fallback"
                session.final_message = "未能进入客服入口（路径变化）"
                return {"success": False, "error": session.final_message, "session": session.to_dict()}

            await self.chat_loop(session)
            ok = session.final_status == "success"
            return {
                "success": ok,
                "status": session.final_status,
                "message": session.final_message,
                "session": session.to_dict(),
            }
        except Exception as exc:
            logger.exception("[unblock_cs] 异常")
            session.final_status = "error"
            session.final_message = str(exc)
            return {"success": False, "error": str(exc), "session": session.to_dict()}
