import asyncio
import copy
import fcntl
import importlib
import json
import re
import sqlite3
import subprocess
import sys
import threading
import time
import traceback
from base64 import b64decode
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

try:
    import jieba
except Exception:
    jieba = None

for _site_path in (
    "/usr/local/lib/python3.12/site-packages",
    "/usr/local/lib/python3.11/site-packages",
):
    if Path(_site_path).exists() and _site_path not in sys.path:
        sys.path.append(_site_path)

try:
    import lark_oapi as lark
except Exception:
    lark = None

_LARK_IMPORT_LOCK = threading.Lock()
_LARK_AUTO_INSTALL_ATTEMPTED = False
_LARK_PACKAGE_SPEC = "lark-oapi>=1.4.0"

try:
    from app.chain.download import DownloadChain
    from app.chain.media import MediaChain
    from app.chain.search import SearchChain
    from app.chain.subscribe import SubscribeChain
    from app.core.event import eventmanager
    from app.core.metainfo import MetaInfo
    from app.db.downloadhistory_oper import DownloadHistoryOper
    from app.db.models.downloadhistory import DownloadHistory
    from app.db.models.transferhistory import TransferHistory
    from app.db.site_oper import SiteOper
    from app.db.subscribe_oper import SubscribeOper
    from app.db.systemconfig_oper import SystemConfigOper
    from app.helper.subscribe import SubscribeHelper
    from app.core.plugin import PluginManager
    from app.log import logger
    from app.scheduler import Scheduler
    from app.schemas.types import EventType, SystemConfigKey, TorrentStatus, media_type_to_agent
    from app.utils.http import RequestUtils
    from app.utils.string import StringUtils
except Exception:
    DownloadChain = None
    DownloadHistoryOper = None
    DownloadHistory = None
    TransferHistory = None
    MediaChain = None
    SearchChain = None
    SiteOper = None
    SubscribeChain = None
    SubscribeHelper = None
    SubscribeOper = None
    SystemConfigOper = None
    eventmanager = None
    MetaInfo = None
    PluginManager = None
    Scheduler = None
    EventType = None
    SystemConfigKey = None
    TorrentStatus = None
    media_type_to_agent = None
    RequestUtils = None
    StringUtils = None

    class _FallbackLogger:
        @staticmethod
        def info(message: str) -> None:
            print(message)

        @staticmethod
        def warning(message: str) -> None:
            print(message)

        @staticmethod
        def error(message: str) -> None:
            print(message)

    logger = _FallbackLogger()


_EVENT_CACHE_FILE = Path("/config/plugins/AgentResourceOfficer/.feishu_event_cache.json")


def ensure_lark_sdk(auto_install: bool = False) -> tuple[bool, str]:
    global lark, _LARK_AUTO_INSTALL_ATTEMPTED

    if lark is not None:
        return True, ""

    with _LARK_IMPORT_LOCK:
        if lark is not None:
            return True, ""

        try:
            import lark_oapi as runtime_lark

            lark = runtime_lark
            return True, ""
        except Exception as exc:
            first_error = str(exc)

        if not auto_install:
            return False, f"缺少依赖 lark-oapi：{first_error}"

        if _LARK_AUTO_INSTALL_ATTEMPTED:
            return False, f"缺少依赖 lark-oapi：{first_error}"

        _LARK_AUTO_INSTALL_ATTEMPTED = True
        requirements_file = Path(__file__).with_name("requirements.txt")
        install_cmds = []
        if requirements_file.exists():
            install_cmds.append([sys.executable, "-m", "pip", "install", "-r", str(requirements_file)])
        install_cmds.append([sys.executable, "-m", "pip", "install", _LARK_PACKAGE_SPEC])

        install_errors: list[str] = []
        for cmd in install_cmds:
            try:
                logger.info(f"[AgentResourceOfficer][Feishu] 正在尝试安装依赖：{' '.join(cmd)}")
                proc = subprocess.run(
                    cmd,
                    capture_output=True,
                    text=True,
                    timeout=180,
                )
            except Exception as exc:
                install_errors.append(str(exc))
                continue
            if proc.returncode == 0:
                try:
                    import lark_oapi as runtime_lark

                    lark = runtime_lark
                    logger.info("[AgentResourceOfficer][Feishu] 已自动安装并加载 lark-oapi")
                    return True, ""
                except Exception as exc:
                    install_errors.append(str(exc))
            else:
                stderr = (proc.stderr or "").strip()
                stdout = (proc.stdout or "").strip()
                install_errors.append(stderr or stdout or f"returncode={proc.returncode}")

        detail = " | ".join([msg for msg in install_errors if msg]) or first_error
        return False, f"缺少依赖 lark-oapi，且自动安装失败：{detail}"


class _FeishuLongConnectionRuntime:
    def __init__(self) -> None:
        self._thread: Optional[threading.Thread] = None
        self._lock = threading.Lock()
        self._fingerprint = ""
        self._channel: Optional["FeishuChannel"] = None

    def start(self, channel: "FeishuChannel") -> None:
        ok, message = ensure_lark_sdk(auto_install=True)
        if not ok:
            logger.error(f"[AgentResourceOfficer][Feishu] {message}")
            return

        if not channel.enabled or not channel.app_id or not channel.app_secret:
            return

        fingerprint = channel.connection_fingerprint()
        with self._lock:
            self._channel = channel
            if self._thread and self._thread.is_alive():
                if fingerprint != self._fingerprint:
                    logger.warning("[AgentResourceOfficer][Feishu] 长连接已在运行，飞书凭证变更需重启 MoviePilot 后生效")
                return
            self._fingerprint = fingerprint
            self._thread = threading.Thread(
                target=self._run,
                name="agent-resource-officer-feishu",
                daemon=True,
            )
            self._thread.start()

    def _run(self) -> None:
        channel = self._channel
        if channel is None or lark is None:
            return

        def _on_message(data) -> None:
            current = self._channel
            if current is not None:
                current.handle_long_connection_event(data)

        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            import lark_oapi.ws.client as lark_ws_client

            lark_ws_client.loop = loop
            event_handler = (
                lark.EventDispatcherHandler.builder("", "")
                .register_p2_im_message_receive_v1(_on_message)
                .build()
            )
            ws_client = lark.ws.Client(
                channel.app_id,
                channel.app_secret,
                log_level=lark.LogLevel.DEBUG if channel.debug else lark.LogLevel.INFO,
                event_handler=event_handler,
            )
            logger.info("[AgentResourceOfficer][Feishu] 正在启动飞书长连接")
            ws_client.start()
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 长连接退出：{exc}\n{traceback.format_exc()}")

    def is_running(self) -> bool:
        with self._lock:
            return bool(self._thread and self._thread.is_alive())

    def stop(self) -> None:
        with self._lock:
            self._channel = None


class FeishuChannel:
    _LEGACY_DEFAULT_COMMANDS = {
        "/p115_manual_transfer",
        "/p115_inc_sync",
        "/p115_full_sync",
        "/p115_strm",
        "/quark_save",
        "/media_search",
        "/media_download",
    }
    _LEGACY_DEFAULT_ALIAS_KEYS = {
        "刮削",
        "搜索",
        "MP搜索",
        "原生搜索",
        "下载",
        "订阅",
        "生成STRM",
        "全量STRM",
        "指定路径STRM",
        "夸克转存",
        "夸克",
        "搜索资源",
        "下载资源",
    }

    def __init__(self, plugin: Any) -> None:
        self.plugin = plugin
        self.runtime = _FeishuLongConnectionRuntime()
        self.enabled = False
        self.allow_all = False
        self.reply_enabled = True
        self.reply_receive_id_type = "chat_id"
        self.app_id = ""
        self.app_secret = ""
        self.verification_token = ""
        self.allowed_chat_ids: List[str] = []
        self.allowed_user_ids: List[str] = []
        self.command_whitelist: List[str] = []
        self.command_aliases = ""
        self.command_mode = "resource_officer"
        self.debug = False
        self._token_cache: Dict[str, Any] = {}
        self._token_lock = threading.Lock()
        self._event_cache: Dict[str, float] = {}
        self._event_lock = threading.Lock()
        self._search_cache: Dict[str, Dict[str, Any]] = {}
        self._search_cache_lock = threading.Lock()
        self._search_cache_limit = 200

    @classmethod
    def default_command_whitelist(cls) -> List[str]:
        return [
            "/pansou_search",
            "/smart_entry",
            "/smart_pick",
            "/media_search",
            "/version",
        ]

    @classmethod
    def default_command_aliases(cls) -> str:
        return (
            "搜索=/smart_entry\n"
            "找=/smart_entry\n"
            "MP搜索=/smart_entry\n"
            "PT搜索=/smart_entry\n"
            "原生搜索=/smart_entry\n"
            "盘搜搜索=/pansou_search\n"
            "盘搜=/pansou_search\n"
            "ps=/pansou_search\n"
            "1=/pansou_search\n"
            "影巢搜索=/smart_entry\n"
            "影巢=/smart_entry\n"
            "yc=/smart_entry\n"
            "2=/smart_entry\n"
            "转存=/smart_entry\n"
            "115转存=/smart_entry\n"
            "夸克转存=/smart_entry\n"
            "夸克=/smart_entry\n"
            "下载=/smart_entry\n"
            "订阅=/smart_entry\n"
            "链接=/smart_entry\n"
            "处理=/smart_entry\n"
            "115登录=/smart_entry\n"
            "115扫码=/smart_entry\n"
            "检查115登录=/smart_entry\n"
            "115登录状态=/smart_entry\n"
            "115状态=/smart_entry\n"
            "115帮助=/smart_entry\n"
            "115任务=/smart_entry\n"
            "继续115任务=/smart_entry\n"
            "取消115任务=/smart_entry\n"
            "影巢签到=/smart_entry\n"
            "影巢普通签到=/smart_entry\n"
            "普通签到=/smart_entry\n"
            "签到=/smart_entry\n"
            "赌狗签到=/smart_entry\n"
            "签到日志=/smart_entry\n"
            "影巢签到日志=/smart_entry\n"
            "选择=/smart_pick\n"
            "详情=/smart_pick\n"
            "审查=/smart_pick\n"
            "选=/smart_pick\n"
            "继续=/smart_pick\n"
            "搜索资源=/smart_entry\n"
            "下载资源=/smart_entry\n"
            "版本=/version"
        )

    @staticmethod
    def clean(value: Any) -> str:
        if value is None:
            return ""
        text = str(value)
        for ch in ("\ufeff", "\u200b", "\u200c", "\u200d", "\u2060", "\ufffc"):
            text = text.replace(ch, "")
        return text.strip()

    @staticmethod
    def split_lines(value: Any) -> List[str]:
        return [line.strip() for line in str(value or "").splitlines() if line.strip()]

    @staticmethod
    def split_commands(value: Any) -> List[str]:
        raw = str(value or "").replace("\n", ",")
        return [item.strip() for item in raw.split(",") if item.strip()]

    @classmethod
    def parse_alias_text(cls, text: str) -> Dict[str, str]:
        result: Dict[str, str] = {}
        for line in str(text or "").splitlines():
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, value = line.split("=", 1)
            key = key.strip()
            value = value.strip()
            if key and value.startswith("/"):
                result[key] = value
        return result

    @classmethod
    def merge_command_aliases(cls, configured_text: str) -> str:
        merged = cls.parse_alias_text(cls.default_command_aliases())
        for key, value in cls.parse_alias_text(configured_text).items():
            if key in cls._LEGACY_DEFAULT_ALIAS_KEYS and value in cls._LEGACY_DEFAULT_COMMANDS:
                continue
            merged[key] = value
        return "\n".join(f"{key}={value}" for key, value in merged.items())

    @classmethod
    def merge_command_whitelist(cls, configured: List[str]) -> List[str]:
        merged: List[str] = []
        seen = set()
        for cmd in configured or []:
            if cmd in cls._LEGACY_DEFAULT_COMMANDS:
                continue
            if cmd and cmd not in seen:
                merged.append(cmd)
                seen.add(cmd)
        for cmd in cls.default_command_whitelist():
            if cmd not in seen:
                merged.append(cmd)
                seen.add(cmd)
        return merged

    def configure(self, config: Dict[str, Any]) -> None:
        self.enabled = bool(config.get("feishu_enabled", False))
        self.allow_all = bool(config.get("feishu_allow_all", False))
        self.reply_enabled = bool(config.get("feishu_reply_enabled", True))
        self.reply_receive_id_type = self.clean(config.get("feishu_reply_receive_id_type") or "chat_id")
        self.app_id = self.clean(config.get("feishu_app_id"))
        self.app_secret = self.clean(config.get("feishu_app_secret"))
        self.verification_token = self.clean(config.get("feishu_verification_token"))
        self.allowed_chat_ids = self.split_lines(config.get("feishu_allowed_chat_ids"))
        self.allowed_user_ids = self.split_lines(config.get("feishu_allowed_user_ids"))
        self.command_whitelist = self.merge_command_whitelist(self.split_commands(config.get("feishu_command_whitelist")))
        self.command_aliases = self.merge_command_aliases(self.clean(config.get("feishu_command_aliases")))
        self.command_mode = self.clean(config.get("feishu_command_mode") or "resource_officer")
        self.debug = bool(config.get("debug", False))

    def start(self) -> None:
        if self.enabled:
            self.runtime.start(self)

    def stop(self) -> None:
        self.runtime.stop()

    def is_running(self) -> bool:
        return self.runtime.is_running()

    @staticmethod
    def is_legacy_bridge_running() -> bool:
        if PluginManager is None:
            return False
        try:
            running_plugins = PluginManager().running_plugins or {}
            plugin = (
                running_plugins.get("FeishuCommandBridgeLong")
                or running_plugins.get("feishucommandbridgelong")
            )
            if not plugin:
                return False
            config_db = Path("/config/user.db")
            if config_db.exists():
                try:
                    with sqlite3.connect(str(config_db)) as conn:
                        row = conn.execute(
                            "select value from systemconfig where key=?",
                            ("plugin.FeishuCommandBridgeLong",),
                        ).fetchone()
                    if row and row[0]:
                        config = json.loads(row[0])
                        if not bool(config.get("enabled")):
                            return False
                except Exception:
                    pass
            # MoviePilot may keep disabled plugins in running_plugins after loading.
            # Treat the legacy bridge as a conflict only when it is actually enabled.
            if hasattr(plugin, "health"):
                try:
                    health = plugin.health()
                    if isinstance(health, dict):
                        return bool(health.get("enabled") and health.get("running"))
                except Exception:
                    pass
            if hasattr(plugin, "_enabled"):
                return bool(getattr(plugin, "_enabled", False))
            if hasattr(plugin, "get_state"):
                try:
                    return bool(plugin.get_state())
                except Exception:
                    return False
            return False
        except Exception:
            return False

    def connection_fingerprint(self) -> str:
        return "|".join([self.app_id, self.app_secret, self.verification_token])

    def health(self) -> Dict[str, Any]:
        sdk_available, sdk_message = ensure_lark_sdk(auto_install=False)
        legacy_bridge_running = self.is_legacy_bridge_running()
        app_id_configured = bool(self.app_id)
        app_secret_configured = bool(self.app_secret)
        verification_token_configured = bool(self.verification_token)
        missing_requirements = []
        if not sdk_available:
            missing_requirements.append("lark-oapi")
        if not app_id_configured:
            missing_requirements.append("feishu_app_id")
        if not app_secret_configured:
            missing_requirements.append("feishu_app_secret")
        conflict_warning = bool(self.enabled and legacy_bridge_running)
        ready_to_start = bool(self.enabled and sdk_available and app_id_configured and app_secret_configured and not conflict_warning)
        safe_to_enable = bool((not legacy_bridge_running) and sdk_available and app_id_configured and app_secret_configured)
        if conflict_warning:
            recommended_action = "disable_legacy_bridge_or_use_different_app"
            migration_hint = "内置飞书入口和旧飞书桥接同时运行，建议关闭旧桥接或使用不同飞书 App。"
        elif not self.enabled and legacy_bridge_running:
            recommended_action = "keep_legacy_or_disable_it_before_migration"
            migration_hint = "内置飞书入口关闭，旧飞书桥接运行中；迁移前先关闭旧桥接。"
        elif not self.enabled:
            recommended_action = "configure_and_enable_feishu_channel"
            migration_hint = "内置飞书入口关闭；配置飞书凭证后可开启。"
        elif missing_requirements:
            recommended_action = "complete_feishu_requirements"
            migration_hint = "内置飞书入口已启用，但依赖或飞书凭证不完整。"
        elif not self.is_running():
            recommended_action = "restart_moviepilot_or_resave_config"
            migration_hint = "内置飞书入口已启用但长连接未运行，建议保存配置或重启 MoviePilot。"
        else:
            recommended_action = "none"
            migration_hint = "内置飞书入口运行正常。"
        return {
            "enabled": self.enabled,
            "running": self.is_running(),
            "sdk_available": sdk_available,
            "app_id_configured": app_id_configured,
            "app_secret_configured": app_secret_configured,
            "verification_token_configured": verification_token_configured,
            "allow_all": self.allow_all,
            "reply_enabled": self.reply_enabled,
            "allowed_chat_count": len(self.allowed_chat_ids),
            "allowed_user_count": len(self.allowed_user_ids),
            "command_mode": self.command_mode,
            "command_whitelist": self.command_whitelist,
            "alias_count": len(self.parse_alias_text(self.command_aliases)),
            "legacy_bridge_running": legacy_bridge_running,
            "conflict_warning": conflict_warning,
            "ready_to_start": ready_to_start,
            "safe_to_enable": safe_to_enable,
            "missing_requirements": missing_requirements,
            "sdk_message": sdk_message,
            "recommended_action": recommended_action,
            "migration_hint": migration_hint,
        }

    def handle_long_connection_event(self, data: Any) -> None:
        if not self.enabled:
            return
        event = getattr(data, "event", None)
        header = getattr(data, "header", None)
        message = getattr(event, "message", None)
        sender = getattr(event, "sender", None)
        sender_id = getattr(sender, "sender_id", None)

        event_id = str(getattr(header, "event_id", "") or "").strip()
        if event_id and self._is_duplicate_event(event_id):
            return
        if not message or str(getattr(message, "message_type", "")).strip() != "text":
            return

        raw_text = self._extract_text(getattr(message, "content", None))
        if not raw_text:
            return
        sender_open_id = str(getattr(sender_id, "open_id", "") or "").strip()
        chat_id = str(getattr(message, "chat_id", "") or "").strip()
        if self.debug:
            logger.info(
                f"[AgentResourceOfficer][Feishu] event_id={event_id} "
                f"chat_id={chat_id} open_id={sender_open_id}"
            )

        if not self._is_allowed(chat_id=chat_id, user_open_id=sender_open_id):
            self.reply_text(chat_id, sender_open_id, "该会话未在白名单中，命令已拒绝。")
            return
        if self._is_help_request(raw_text):
            self.reply_text(chat_id, sender_open_id, self._build_help_text())
            return
        if self._is_menu_request(raw_text):
            self.reply_text(chat_id, sender_open_id, self._build_menu_text())
            return

        command_text = self._map_text_to_command(raw_text)
        if not command_text:
            return
        cmd = command_text.split()[0]
        if cmd not in self.command_whitelist:
            self.reply_text(chat_id, sender_open_id, f"命令 {cmd} 不在白名单中。\n\n{self._build_help_text()}")
            return
        if not self._handle_builtin_command(command_text, chat_id, sender_open_id):
            self._submit_moviepilot_command(command_text, chat_id, sender_open_id)

    def _handle_builtin_command(self, command_text: str, chat_id: str, open_id: str) -> bool:
        parts = command_text.split(maxsplit=1)
        cmd = parts[0].strip()
        arg = parts[1].strip() if len(parts) > 1 else ""
        cache_key = self._cache_key(chat_id, open_id)

        if cmd == "/version":
            self.reply_text(chat_id, open_id, f"Agent影视助手 {getattr(self.plugin, 'plugin_version', '')}\n飞书入口：{'运行中' if self.is_running() else '未运行'}")
            return True

        if cmd == "/media_search":
            if not arg:
                self.reply_text(chat_id, open_id, "用法：MP搜索 片名")
                return True
            self.reply_text(chat_id, open_id, f"正在使用 MP 原生搜索：{arg}")
            self._run_thread("feishu-media-search", self._run_media_search, arg, chat_id, open_id)
            return True

        if cmd == "/media_download":
            if not arg or not arg.isdigit():
                self.reply_text(chat_id, open_id, "用法：下载 序号\n示例：下载 1")
                return True
            self.reply_text(chat_id, open_id, f"正在下载第 {arg} 条 PT 结果，请稍候。")
            self._run_thread("feishu-media-download", self._run_media_download, int(arg), chat_id, open_id)
            return True

        if cmd in {"/media_subscribe", "/media_subscribe_search"}:
            if not arg:
                self.reply_text(chat_id, open_id, "用法：订阅 片名\n示例：订阅 流浪地球2")
                return True
            if cmd == "/media_subscribe_search":
                self.reply_text(
                    chat_id,
                    open_id,
                    "“订阅并搜索 片名”旧命令已取消。\n请改用：订阅 片名\n如需立即看资源，再手动发 MP搜索 / 盘搜搜索 / 影巢搜索。",
                )
                return True
            self.reply_text(chat_id, open_id, f"正在订阅：{arg}")
            self._run_thread("feishu-media-subscribe", self._run_media_subscribe, arg, False, chat_id, open_id)
            return True

        if cmd == "/pansou_search":
            if not arg:
                self.reply_text(chat_id, open_id, "用法：盘搜搜索 片名\n示例：盘搜搜索 流浪地球2")
                return True
            self.reply_text(chat_id, open_id, f"正在使用盘搜搜索：{arg}")
            self._run_thread("feishu-pansou-search", self._run_assistant_route, f"盘搜搜索 {arg}", cache_key, chat_id, open_id)
            return True

        if cmd in {"/smart_entry", "/quark_save"}:
            if not arg:
                self.reply_text(chat_id, open_id, "用法：处理 片名 或 处理 分享链接")
                return True
            self.reply_text(chat_id, open_id, f"正在智能处理：{arg}")
            self._run_thread("feishu-smart-entry", self._run_assistant_route, arg, cache_key, chat_id, open_id)
            return True

        if cmd == "/smart_pick":
            if not arg:
                self.reply_text(chat_id, open_id, "用法：选择 序号\n示例：选择 1\n也支持：详情、审查、n 下一页")
                return True
            self.reply_text(chat_id, open_id, f"正在继续执行：{arg}")
            self._run_thread("feishu-smart-pick", self._run_assistant_pick, arg, cache_key, chat_id, open_id)
            return True

        if cmd == "/p115_manual_transfer":
            if not arg:
                paths = self._get_p115_manual_transfer_paths()
                if not paths:
                    self.reply_text(chat_id, open_id, "未配置待整理目录。请先在 P115StrmHelper 中配置 pan_transfer_paths，或发送：刮削 /待整理/")
                    return True
                self.reply_text(chat_id, open_id, f"已开始刮削 {len(paths)} 个目录：\n" + "\n".join(f"- {path}" for path in paths))
                self._run_thread("feishu-p115-manual-transfer-batch", self._run_p115_manual_transfer_batch, paths, chat_id, open_id)
                return True
            self.reply_text(chat_id, open_id, f"已开始刮削：{arg}")
            self._run_thread("feishu-p115-manual-transfer", self._run_p115_manual_transfer, arg, chat_id, open_id)
            return True

        if cmd in {"/p115_inc_sync", "/p115_full_sync", "/p115_strm"}:
            final_command = "/p115_full_sync" if cmd == "/p115_strm" and not arg else command_text
            self._submit_p115_command(final_command, chat_id, open_id)
            return True

        return False

    @staticmethod
    def _run_thread(name: str, target: Any, *args: Any) -> None:
        threading.Thread(target=target, args=args, name=name, daemon=True).start()

    def _run_assistant_route(self, text: str, session: str, chat_id: str, open_id: str) -> None:
        result = self.plugin.feishu_assistant_route(text=text, session=session)
        self._reply_result(chat_id, open_id, result)

    def _run_assistant_pick(self, arg: str, session: str, chat_id: str, open_id: str) -> None:
        result = self.plugin.feishu_assistant_pick(arg=arg, session=session)
        self._reply_result(chat_id, open_id, result)

    def _reply_result(self, chat_id: str, open_id: str, result: Dict[str, Any]) -> None:
        message = str(result.get("message") or "处理完成").strip()
        self.reply_text(chat_id, open_id, message)
        qrcode = self._find_nested_value(result.get("data"), "qrcode")
        if isinstance(qrcode, str):
            self.reply_qrcode_data_url(chat_id, open_id, qrcode)

    @classmethod
    def _find_nested_value(cls, payload: Any, key: str) -> Any:
        if isinstance(payload, dict):
            if key in payload:
                return payload.get(key)
            for value in payload.values():
                found = cls._find_nested_value(value, key)
                if found:
                    return found
        elif isinstance(payload, list):
            for value in payload:
                found = cls._find_nested_value(value, key)
                if found:
                    return found
        return None

    def _run_media_search(self, keyword: str, chat_id: str, open_id: str) -> None:
        self.reply_text(chat_id, open_id, self._execute_media_search(keyword, self._cache_key(chat_id, open_id)))

    def _run_media_download(self, index: int, chat_id: str, open_id: str) -> None:
        result = self.plugin.feishu_assistant_route(
            text=f"下载 {index}",
            session=self._cache_key(chat_id, open_id),
        )
        self._reply_result(chat_id, open_id, result)

    def _run_media_subscribe(self, keyword: str, immediate: bool, chat_id: str, open_id: str) -> None:
        self.reply_text(chat_id, open_id, self._execute_media_subscribe(keyword, immediate))

    def _execute_media_search(self, keyword: str, cache_key: str) -> str:
        if not all([MetaInfo, MediaChain, SearchChain, StringUtils]):
            return "MP 原生搜索失败：当前环境缺少 MoviePilot 搜索依赖。"
        try:
            meta = MetaInfo(keyword)
            mediainfo = MediaChain().recognize_media(meta=meta)
            if not mediainfo:
                return f"未识别到媒体信息：{keyword}"
            season = meta.begin_season if meta.begin_season else mediainfo.season
            results = SearchChain().search_by_id(
                tmdbid=mediainfo.tmdb_id,
                doubanid=mediainfo.douban_id,
                mtype=mediainfo.type,
                season=season,
                cache_local=False,
            ) or []
            if not results:
                return f"已识别 {self._format_media_label(mediainfo, season)}，但暂未搜索到资源。"
            self._set_search_cache(cache_key, keyword, mediainfo, results)
            preview_limit = 10
            preview_results = results[:preview_limit]
            lines = [
                f"已识别：{self._format_media_label(mediainfo, season)}",
                f"共找到 {len(results)} 条资源，展示前 {len(preview_results)} 条：",
            ]
            for idx, context in enumerate(preview_results, start=1):
                torrent = context.torrent_info
                title = str(torrent.title or "").strip()
                size = StringUtils.str_filesize(torrent.size) if torrent.size else "未知"
                seeders = torrent.seeders if torrent.seeders is not None else "?"
                site = torrent.site_name or "未知站点"
                volume = torrent.volume_factor if getattr(torrent, "volume_factor", None) else "未知"
                lines.append(f"{idx}. [{site}] {title}")
                lines.append(f"   大小：{size} | 做种：{seeders} | 促销：{volume}")
            lines.append("下一步：回复“下载 序号”会直接下载当前 PT 结果。")
            lines.append("如需长期跟踪，回复“订阅 片名”。")
            return "\n".join(lines)
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 搜索资源失败：{keyword} {exc}\n{traceback.format_exc()}")
            return f"搜索资源失败：{keyword}\n错误：{exc}"

    def _query_media_detail(self, keyword: str, media_type: str = "", year: str = "") -> Dict[str, Any]:
        if not all([MetaInfo, MediaChain]):
            return {"success": False, "message": "媒体识别失败：当前环境缺少 MoviePilot 媒体识别依赖。", "item": {}}
        title_text = str(keyword or "").strip()
        if not title_text:
            return {"success": False, "message": "媒体识别失败：缺少片名。", "item": {}}
        try:
            meta = MetaInfo(title_text)
            if year:
                try:
                    meta.year = str(year)
                except Exception:
                    pass
            mediainfo = MediaChain().recognize_media(meta=meta)
            if not mediainfo:
                return {"success": False, "message": f"未识别到媒体信息：{title_text}", "item": {"keyword": title_text}}
            season = meta.begin_season if meta.begin_season else getattr(mediainfo, "season", None)
            media_type_value = getattr(mediainfo, "type", None)
            media_type_name = getattr(media_type_value, "name", "") or str(media_type_value or "")
            item = {
                "keyword": title_text,
                "title": str(getattr(mediainfo, "title", "") or ""),
                "original_title": str(getattr(mediainfo, "original_title", "") or ""),
                "year": str(getattr(mediainfo, "year", "") or ""),
                "type": media_type_name,
                "tmdb_id": getattr(mediainfo, "tmdb_id", None),
                "douban_id": getattr(mediainfo, "douban_id", None),
                "imdb_id": str(getattr(mediainfo, "imdb_id", "") or ""),
                "season": season,
                "category": str(getattr(mediainfo, "category", "") or ""),
                "overview": str(getattr(mediainfo, "overview", "") or "")[:300],
            }
            lines = [
                f"媒体识别：{title_text}",
                f"结果：{item.get('title') or '-'} ({item.get('year') or '-'})",
                f"类型：{item.get('type') or '-'} | TMDB：{item.get('tmdb_id') or '-'} | 豆瓣：{item.get('douban_id') or '-'}",
            ]
            if item.get("original_title") and item.get("original_title") != item.get("title"):
                lines.append(f"原标题：{item.get('original_title')}")
            if season:
                lines.append(f"季：S{int(season):02d}" if isinstance(season, int) else f"季：{season}")
            if item.get("overview"):
                lines.append(f"简介：{item.get('overview')}")
            lines.append("说明：这是 MoviePilot 原生识别结果，后续 MP 搜索、订阅和 PT 评分会以它为准。")
            return {"success": True, "message": "\n".join(lines), "item": item}
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 媒体识别失败：{title_text} {exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"媒体识别失败：{exc}", "item": {"keyword": title_text}}

    def _execute_media_download(self, index: int, cache_key: str) -> str:
        if DownloadChain is None:
            return "下载资源失败：当前环境缺少 MoviePilot 下载依赖。"
        cache = self._get_search_cache(cache_key)
        if not cache:
            return "没有可用的搜索缓存，请先发送：MP搜索 片名"
        results = cache.get("results") or []
        if index < 1 or index > len(results):
            return f"序号超出范围，请输入 1 到 {len(results)} 之间的数字。"
        context = copy.deepcopy(results[index - 1])
        torrent = context.torrent_info
        try:
            save_path = ""
            if self.plugin is not None:
                save_path = str(getattr(self.plugin, "_mp_download_save_path", "") or "").strip()
            download_id = DownloadChain().download_single(
                context=context,
                username="agentresourceofficer-feishu",
                source="AgentResourceOfficer",
                save_path=save_path or None,
            )
            if not download_id:
                return f"下载提交失败：{torrent.title}"
            path_line = f"\n保存路径：{save_path}" if save_path else ""
            return f"已提交下载：{torrent.title}\n站点：{torrent.site_name or '未知站点'}{path_line}\n任务ID：{download_id}"
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 下载资源失败：{torrent.title} {exc}\n{traceback.format_exc()}")
            return f"下载资源失败：{torrent.title}\n错误：{exc}"

    def _query_download_tasks(
        self,
        *,
        downloader: str = "",
        status: str = "downloading",
        title: str = "",
        hash_value: str = "",
        limit: int = 10,
    ) -> Dict[str, Any]:
        if DownloadChain is None:
            return {"success": False, "message": "查询下载任务失败：当前环境缺少 MoviePilot 下载依赖。", "items": []}
        try:
            chain = DownloadChain()
            status_name = str(status or "downloading").strip().lower()
            downloader_name = str(downloader or "").strip() or None
            tasks: List[Any] = []
            if hash_value:
                tasks = chain.list_torrents(downloader=downloader_name, hashs=[hash_value]) or []
            elif status_name == "downloading":
                tasks = chain.downloading(name=downloader_name) or []
            else:
                for torrent_status in [TorrentStatus.DOWNLOADING, TorrentStatus.TRANSFER] if TorrentStatus else []:
                    tasks.extend(chain.list_torrents(downloader=downloader_name, status=torrent_status) or [])
            if status_name == "completed":
                tasks = [task for task in tasks if str(getattr(task, "state", "") or "").lower() in {"seeding", "completed"}]
            elif status_name == "paused":
                tasks = [task for task in tasks if str(getattr(task, "state", "") or "").lower() == "paused"]
            if title:
                title_lower = title.lower()
                tasks = [
                    task for task in tasks
                    if title_lower in str(getattr(task, "title", "") or getattr(task, "name", "") or "").lower()
                ]
            items: List[Dict[str, Any]] = []
            for index, task in enumerate(tasks[:max(1, min(30, int(limit or 10)))], 1):
                task_hash = str(getattr(task, "hash", "") or "")
                history = DownloadHistoryOper().get_by_hash(task_hash) if DownloadHistoryOper and task_hash else None
                title_text = str(getattr(task, "title", "") or getattr(task, "name", "") or "").strip()
                if history and getattr(history, "title", None):
                    title_text = title_text or str(history.title)
                size_value = getattr(task, "size", None)
                size_text = StringUtils.str_filesize(size_value) if StringUtils and size_value else ""
                progress = getattr(task, "progress", None)
                try:
                    progress_text = f"{float(progress):.1f}%" if progress is not None else ""
                except Exception:
                    progress_text = str(progress or "")
                items.append({
                    "index": index,
                    "hash": task_hash,
                    "hash_short": task_hash[:8],
                    "downloader": str(getattr(task, "downloader", "") or ""),
                    "title": title_text or "未命名任务",
                    "name": str(getattr(task, "name", "") or ""),
                    "size": size_text,
                    "progress": progress_text,
                    "state": str(getattr(task, "state", "") or ""),
                    "dlspeed": getattr(task, "dlspeed", None),
                    "upspeed": getattr(task, "upspeed", None),
                    "left_time": getattr(task, "left_time", None),
                    "tags": str(getattr(task, "tags", "") or ""),
                    "media_title": str(getattr(history, "title", "") or "") if history else "",
                })
            status_label = {
                "downloading": "下载中",
                "completed": "已完成",
                "paused": "已暂停",
                "all": "全部",
            }.get(status_name, status_name)
            if not items:
                return {
                    "success": True,
                    "message": f"未找到{status_label}下载任务。",
                    "items": [],
                    "total": len(tasks),
                    "status": status_name,
                }
            lines = [f"下载任务：{status_label}，共 {len(tasks)} 条，展示前 {len(items)} 条："]
            for item in items:
                details = [
                    item.get("progress") or "进度未知",
                    item.get("size") or "大小未知",
                    item.get("state") or "状态未知",
                    f"下载器:{item.get('downloader') or '默认'}",
                    f"Hash:{item.get('hash_short')}",
                ]
                lines.append(f"{item.get('index')}. {item.get('title')}")
                lines.append("   " + " | ".join(details))
            lines.append("写入操作需确认：可发“暂停下载 1”“恢复下载 1”“删除下载 1”。")
            return {
                "success": True,
                "message": "\n".join(lines),
                "items": items,
                "total": len(tasks),
                "status": status_name,
            }
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 查询下载任务失败：{exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"查询下载任务失败：{exc}", "items": []}

    def _control_download_task(
        self,
        *,
        action: str,
        hash_value: str,
        downloader: str = "",
        delete_files: bool = False,
    ) -> Dict[str, Any]:
        if DownloadChain is None:
            return {"success": False, "message": "操作下载任务失败：当前环境缺少 MoviePilot 下载依赖。"}
        task_hash = str(hash_value or "").strip()
        if len(task_hash) != 40 or not all(ch in "0123456789abcdefABCDEF" for ch in task_hash):
            return {"success": False, "message": "操作下载任务失败：hash 格式无效，请先查询下载任务后按编号操作。"}
        downloader_name = str(downloader or "").strip() or None
        action_name = str(action or "").strip().lower()
        try:
            chain = DownloadChain()
            if action_name in {"pause", "stop"}:
                ok = chain.set_downloading(task_hash, "stop", name=downloader_name)
                label = "暂停"
            elif action_name in {"resume", "start"}:
                ok = chain.set_downloading(task_hash, "start", name=downloader_name)
                label = "恢复"
            elif action_name in {"delete", "remove"}:
                ok = chain.remove_torrents(hashs=[task_hash], downloader=downloader_name, delete_file=bool(delete_files))
                label = "删除"
            else:
                return {"success": False, "message": f"操作下载任务失败：不支持的动作 {action}"}
            suffix = "（包含文件）" if action_name in {"delete", "remove"} and delete_files else ""
            return {
                "success": bool(ok),
                "message": f"{label}下载任务{'成功' if ok else '失败'}：{task_hash[:8]}{suffix}",
                "hash": task_hash,
                "downloader": downloader_name or "",
                "action": action_name,
            }
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 操作下载任务失败：{task_hash} {exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"操作下载任务失败：{exc}"}

    def _query_downloaders(self) -> Dict[str, Any]:
        if SystemConfigOper is None or SystemConfigKey is None:
            return {"success": False, "message": "查询下载器失败：当前环境缺少 MoviePilot 配置依赖。", "items": []}
        try:
            raw_items = SystemConfigOper().get(SystemConfigKey.Downloaders) or []
            items: List[Dict[str, Any]] = []
            for index, item in enumerate(raw_items, 1):
                if not isinstance(item, dict):
                    continue
                items.append({
                    "index": index,
                    "name": str(item.get("name") or ""),
                    "type": str(item.get("type") or ""),
                    "enabled": bool(item.get("enabled")),
                    "default": bool(item.get("default")),
                })
            enabled = [item for item in items if item.get("enabled")]
            if not items:
                return {"success": True, "message": "未配置下载器。", "items": [], "enabled_count": 0}
            lines = [f"下载器配置：共 {len(items)} 个，启用 {len(enabled)} 个"]
            for item in items:
                status = "启用" if item.get("enabled") else "停用"
                default = "，默认" if item.get("default") else ""
                lines.append(f"{item.get('index')}. {item.get('name') or '-'} | {item.get('type') or '-'} | {status}{default}")
            return {
                "success": True,
                "message": "\n".join(lines),
                "items": items,
                "enabled_count": len(enabled),
            }
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 查询下载器失败：{exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"查询下载器失败：{exc}", "items": []}

    def _query_sites(self, *, status: str = "active", name: str = "", limit: int = 30) -> Dict[str, Any]:
        if SiteOper is None:
            return {"success": False, "message": "查询站点失败：当前环境缺少 MoviePilot 站点依赖。", "items": []}
        try:
            status_name = str(status or "active").strip().lower()
            name_filter = str(name or "").strip().lower()
            sites = SiteOper().list_order_by_pri() or []
            items: List[Dict[str, Any]] = []
            for site in sites:
                is_active = bool(getattr(site, "is_active", False))
                if status_name == "active" and not is_active:
                    continue
                if status_name == "inactive" and is_active:
                    continue
                site_name = str(getattr(site, "name", "") or "")
                if name_filter and name_filter not in site_name.lower():
                    continue
                cookie = str(getattr(site, "cookie", "") or "")
                items.append({
                    "index": len(items) + 1,
                    "id": getattr(site, "id", None),
                    "name": site_name,
                    "domain": str(getattr(site, "domain", "") or ""),
                    "url": str(getattr(site, "url", "") or ""),
                    "pri": getattr(site, "pri", None),
                    "is_active": is_active,
                    "has_cookie": bool(cookie),
                    "downloader": str(getattr(site, "downloader", "") or ""),
                    "proxy": bool(getattr(site, "proxy", False)),
                    "timeout": getattr(site, "timeout", None),
                })
            total = len(items)
            items = items[:max(1, min(100, int(limit or 30)))]
            label = {"active": "已启用", "inactive": "已停用", "all": "全部"}.get(status_name, status_name)
            if not items:
                return {"success": True, "message": f"未找到{label}站点。", "items": [], "total": total}
            lines = [f"PT 站点：{label}，共 {total} 个，展示前 {len(items)} 个："]
            for item in items:
                cookie_state = "有Cookie" if item.get("has_cookie") else "无Cookie"
                active_state = "启用" if item.get("is_active") else "停用"
                lines.append(
                    f"{item.get('index')}. {item.get('name') or '-'} | {item.get('domain') or '-'} | "
                    f"{active_state} | {cookie_state} | 优先级:{item.get('pri')} | 下载器:{item.get('downloader') or '默认'}"
                )
            lines.append("说明：这里不会返回 Cookie 明文；如站点搜索失败，优先检查是否启用、Cookie 是否存在、站点绑定下载器是否可用。")
            return {
                "success": True,
                "message": "\n".join(lines),
                "items": items,
                "total": total,
                "status": status_name,
            }
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 查询站点失败：{exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"查询站点失败：{exc}", "items": []}

    def _query_subscribes(
        self,
        *,
        status: str = "all",
        media_type: str = "all",
        name: str = "",
        limit: int = 20,
    ) -> Dict[str, Any]:
        if SubscribeOper is None:
            return {"success": False, "message": "查询订阅失败：当前环境缺少 MoviePilot 订阅依赖。", "items": []}
        try:
            status_name = str(status or "all").strip()
            media_type_name = str(media_type or "all").strip().lower()
            name_filter = str(name or "").strip().lower()
            subscribes = SubscribeOper().list() or []
            items: List[Dict[str, Any]] = []
            for sub in subscribes:
                state = str(getattr(sub, "state", "") or "")
                if status_name != "all" and state != status_name:
                    continue
                sub_type = str(getattr(sub, "type", "") or "").lower()
                if media_type_name != "all" and media_type_name not in {sub_type, "movie" if sub_type == "电影" else sub_type, "tv" if sub_type == "电视剧" else sub_type}:
                    continue
                title = str(getattr(sub, "name", "") or "")
                if name_filter and name_filter not in title.lower():
                    continue
                items.append({
                    "index": len(items) + 1,
                    "id": getattr(sub, "id", None),
                    "name": title or "未命名订阅",
                    "year": str(getattr(sub, "year", "") or ""),
                    "type": str(getattr(sub, "type", "") or ""),
                    "season": getattr(sub, "season", None),
                    "state": state,
                    "total_episode": getattr(sub, "total_episode", None),
                    "lack_episode": getattr(sub, "lack_episode", None),
                    "start_episode": getattr(sub, "start_episode", None),
                    "quality": str(getattr(sub, "quality", "") or ""),
                    "resolution": str(getattr(sub, "resolution", "") or ""),
                    "effect": str(getattr(sub, "effect", "") or ""),
                    "include": str(getattr(sub, "include", "") or ""),
                    "exclude": str(getattr(sub, "exclude", "") or ""),
                    "sites": getattr(sub, "sites", None),
                    "downloader": str(getattr(sub, "downloader", "") or ""),
                    "save_path": str(getattr(sub, "save_path", "") or ""),
                    "best_version": getattr(sub, "best_version", None),
                    "tmdbid": getattr(sub, "tmdbid", None),
                    "doubanid": str(getattr(sub, "doubanid", "") or ""),
                    "last_update": str(getattr(sub, "last_update", "") or ""),
                })
            total = len(items)
            items = items[:max(1, min(100, int(limit or 20)))]
            status_label = {"R": "启用", "S": "暂停", "P": "待处理", "N": "完成", "all": "全部"}.get(status_name, status_name)
            if not items:
                return {"success": True, "message": f"未找到{status_label}订阅。", "items": [], "total": total}
            lines = [f"MP 订阅：{status_label}，共 {total} 条，展示前 {len(items)} 条："]
            for item in items:
                season = f" S{int(item.get('season')):02d}" if item.get("season") else ""
                lack = item.get("lack_episode")
                lack_text = f"缺 {lack} 集" if lack not in (None, "", 0) else "无缺集"
                filters = " / ".join(value for value in [item.get("resolution"), item.get("effect"), item.get("quality")] if value) or "默认规则"
                lines.append(f"{item.get('index')}. #{item.get('id')} {item.get('name')} ({item.get('year') or '-'}){season}")
                lines.append(f"   状态:{item.get('state') or '-'} | {lack_text} | 规则:{filters} | 下载器:{item.get('downloader') or '默认'}")
            lines.append("写入操作需确认：可发“刷新订阅 1”“暂停订阅 1”“恢复订阅 1”“删除订阅 1”。")
            return {"success": True, "message": "\n".join(lines), "items": items, "total": total, "status": status_name}
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 查询订阅失败：{exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"查询订阅失败：{exc}", "items": []}

    def _control_subscribe(self, *, action: str, subscribe_id: int) -> Dict[str, Any]:
        if SubscribeOper is None:
            return {"success": False, "message": "操作订阅失败：当前环境缺少 MoviePilot 订阅依赖。"}
        sid = int(subscribe_id or 0)
        if sid <= 0:
            return {"success": False, "message": "操作订阅失败：订阅 ID 无效。"}
        action_name = str(action or "").strip().lower()
        try:
            oper = SubscribeOper()
            sub = oper.get(sid)
            if not sub:
                return {"success": False, "message": f"操作订阅失败：订阅 #{sid} 不存在。"}
            old_info = sub.to_dict() if hasattr(sub, "to_dict") else {}
            if action_name in {"search", "run"}:
                if Scheduler is None:
                    return {"success": False, "message": "刷新订阅失败：当前环境缺少调度器。"}
                Scheduler().start(job_id="subscribe_search", **{"sid": sid, "state": None, "manual": True})
                return {"success": True, "message": f"已触发订阅刷新：#{sid} {getattr(sub, 'name', '')}", "subscribe_id": sid, "action": action_name}
            if action_name in {"pause", "stop"}:
                updated = oper.update(sid, {"state": "S"})
                label = "暂停"
            elif action_name in {"resume", "start"}:
                updated = oper.update(sid, {"state": "R"})
                label = "恢复"
            elif action_name in {"delete", "remove"}:
                sub_name = str(getattr(sub, "name", "") or "")
                sub_year = str(getattr(sub, "year", "") or "")
                oper.delete(sid)
                if eventmanager and EventType:
                    eventmanager.send_event(EventType.SubscribeDeleted, {"subscribe_id": sid, "subscribe_info": old_info})
                if SubscribeHelper:
                    SubscribeHelper().sub_done_async({"tmdbid": getattr(sub, "tmdbid", None), "doubanid": getattr(sub, "doubanid", None)})
                return {"success": True, "message": f"成功删除订阅：#{sid} {sub_name} ({sub_year})", "subscribe_id": sid, "action": action_name}
            else:
                return {"success": False, "message": f"操作订阅失败：不支持的动作 {action}"}
            if eventmanager and EventType:
                eventmanager.send_event(EventType.SubscribeModified, {
                    "subscribe_id": sid,
                    "old_subscribe_info": old_info,
                    "subscribe_info": updated.to_dict() if updated and hasattr(updated, "to_dict") else {},
                })
            return {"success": True, "message": f"{label}订阅成功：#{sid} {getattr(sub, 'name', '')}", "subscribe_id": sid, "action": action_name}
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 操作订阅失败：{sid} {exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"操作订阅失败：{exc}", "subscribe_id": sid}

    @staticmethod
    def _path_preview(value: Any, max_parts: int = 4) -> str:
        text = str(value or "").strip()
        if not text:
            return ""
        normalized = text.replace("\\", "/")
        parts = [part for part in normalized.split("/") if part]
        if len(parts) <= max_parts:
            return normalized
        prefix = "/" if normalized.startswith("/") else ""
        return f"{prefix}.../" + "/".join(parts[-max_parts:])

    @staticmethod
    def _transfer_status_bool(status: str) -> Optional[bool]:
        name = str(status or "all").strip().lower()
        if name in {"success", "succeeded", "ok", "true", "成功", "已成功"}:
            return True
        if name in {"failed", "fail", "error", "false", "失败", "错误"}:
            return False
        return None

    def _query_download_history(
        self,
        *,
        title: str = "",
        hash_value: str = "",
        limit: int = 10,
        page: int = 1,
    ) -> Dict[str, Any]:
        if DownloadHistory is None or DownloadHistoryOper is None:
            return {"success": False, "message": "查询下载历史失败：当前环境缺少 MoviePilot 下载历史依赖。", "items": []}
        try:
            page_num = max(1, int(page or 1))
            page_size = max(1, min(50, int(limit or 10)))
            title_text = str(title or "").strip()
            hash_text = str(hash_value or "").strip()
            oper = DownloadHistoryOper()
            db = getattr(oper, "_db", None)
            if db is None:
                records = oper.list_by_page(page=1, count=500) or []
                if title_text:
                    title_lower = title_text.lower()
                    records = [
                        item for item in records
                        if title_lower in str(getattr(item, "title", "") or "").lower()
                        or title_lower in str(getattr(item, "torrent_name", "") or "").lower()
                        or title_lower in str(getattr(item, "path", "") or "").lower()
                    ]
                if hash_text:
                    records = [
                        item for item in records
                        if str(getattr(item, "download_hash", "") or "").lower().startswith(hash_text.lower())
                    ]
                total = len(records)
                selected_records = records[(page_num - 1) * page_size:(page_num - 1) * page_size + page_size]
            else:
                query = db.query(DownloadHistory)
                if title_text:
                    like = f"%{title_text}%"
                    query = query.filter(
                        DownloadHistory.title.like(like)
                        | DownloadHistory.torrent_name.like(like)
                        | DownloadHistory.path.like(like)
                    )
                if hash_text:
                    query = query.filter(DownloadHistory.download_hash.like(f"{hash_text}%"))
                query = query.order_by(DownloadHistory.date.desc(), DownloadHistory.id.desc())
                total = query.count()
                selected_records = query.offset((page_num - 1) * page_size).limit(page_size).all()

            items: List[Dict[str, Any]] = []
            for index, record in enumerate(selected_records, start=(page_num - 1) * page_size + 1):
                task_hash = str(getattr(record, "download_hash", "") or "")
                transfer_records = TransferHistory.list_by_hash(download_hash=task_hash) if TransferHistory is not None and task_hash else []
                transfer_success = any(bool(getattr(item, "status", False)) for item in transfer_records or [])
                transfer_failed = any(not bool(getattr(item, "status", False)) for item in transfer_records or [])
                if transfer_success:
                    transfer_status = "success"
                    transfer_status_text = "已入库"
                elif transfer_failed:
                    transfer_status = "failed"
                    transfer_status_text = "整理失败"
                else:
                    transfer_status = "none"
                    transfer_status_text = "未见整理记录"
                transfer_dest = ""
                transfer_error = ""
                if transfer_records:
                    first_transfer = transfer_records[0]
                    transfer_dest = self._path_preview(getattr(first_transfer, "dest", ""))
                    transfer_error = str(getattr(first_transfer, "errmsg", "") or "")[:300]
                item = {
                    "index": index,
                    "id": getattr(record, "id", None),
                    "title": str(getattr(record, "title", "") or "未命名媒体"),
                    "year": str(getattr(record, "year", "") or ""),
                    "type": str(getattr(record, "type", "") or ""),
                    "season": str(getattr(record, "seasons", "") or ""),
                    "episode": str(getattr(record, "episodes", "") or ""),
                    "date": str(getattr(record, "date", "") or ""),
                    "downloader": str(getattr(record, "downloader", "") or ""),
                    "download_hash": task_hash,
                    "download_hash_short": task_hash[:8],
                    "torrent_name": str(getattr(record, "torrent_name", "") or ""),
                    "torrent_site": str(getattr(record, "torrent_site", "") or ""),
                    "username": str(getattr(record, "username", "") or ""),
                    "channel": str(getattr(record, "channel", "") or ""),
                    "path_preview": self._path_preview(getattr(record, "path", "")),
                    "tmdbid": getattr(record, "tmdbid", None),
                    "doubanid": str(getattr(record, "doubanid", "") or ""),
                    "transfer_status": transfer_status,
                    "transfer_status_text": transfer_status_text,
                    "transfer_count": len(transfer_records or []),
                    "transfer_dest_preview": transfer_dest,
                }
                if transfer_error and transfer_status == "failed":
                    item["transfer_error"] = transfer_error
                items.append(item)

            title_label = f"：{title_text or hash_text}" if title_text or hash_text else ""
            if not items:
                return {
                    "success": True,
                    "message": f"未找到下载历史{title_label}。",
                    "items": [],
                    "total": total,
                    "page": page_num,
                    "limit": page_size,
                }
            total_pages = (total + page_size - 1) // page_size if total else 1
            lines = [f"下载历史{title_label}：第 {page_num}/{total_pages} 页，共 {total} 条，展示 {len(items)} 条："]
            for item in items:
                season_episode = " ".join(value for value in [item.get("season"), item.get("episode")] if value)
                lines.append(f"{item.get('index')}. {item.get('title')} ({item.get('year') or '-'}) {season_episode}".rstrip())
                details = [
                    item.get("date") or "-",
                    f"站点:{item.get('torrent_site') or '-'}",
                    f"下载器:{item.get('downloader') or '默认'}",
                    f"Hash:{item.get('download_hash_short') or '-'}",
                    f"整理:{item.get('transfer_status_text')}",
                ]
                lines.append("   " + " | ".join(details))
                if item.get("path_preview"):
                    lines.append(f"   保存：{item.get('path_preview')}")
                if item.get("transfer_dest_preview"):
                    lines.append(f"   入库：{item.get('transfer_dest_preview')}")
                if item.get("transfer_error"):
                    lines.append(f"   整理错误：{item.get('transfer_error')}")
            lines.append("说明：这是只读查询，用于追踪下载提交后是否进入整理流程。")
            return {
                "success": True,
                "message": "\n".join(lines),
                "items": items,
                "total": total,
                "page": page_num,
                "limit": page_size,
            }
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 查询下载历史失败：{exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"查询下载历史失败：{exc}", "items": []}

    def _query_transfer_history(
        self,
        *,
        title: str = "",
        status: str = "all",
        limit: int = 10,
        page: int = 1,
    ) -> Dict[str, Any]:
        if TransferHistory is None:
            return {"success": False, "message": "查询整理历史失败：当前环境缺少 MoviePilot 整理历史依赖。", "items": []}
        try:
            page_num = max(1, int(page or 1))
            page_size = max(1, min(50, int(limit or 10)))
            status_bool = self._transfer_status_bool(status)
            title_text = str(title or "").strip()
            search_text = title_text
            if title_text and jieba is not None:
                try:
                    search_text = "%".join(jieba.cut(title_text, HMM=False))
                except Exception:
                    search_text = title_text

            if search_text:
                records = TransferHistory.list_by_title(title=search_text, page=1, count=-1, status=None) or []
                if status_bool is not None:
                    records = [item for item in records if bool(getattr(item, "status", False)) is status_bool]
            else:
                records = TransferHistory.list_by_page(page=1, count=-1, status=status_bool) or []

            total = len(records)
            start = (page_num - 1) * page_size
            selected_records = records[start:start + page_size]
            items: List[Dict[str, Any]] = []
            for index, record in enumerate(selected_records, start=start + 1):
                media_type = str(getattr(record, "type", "") or "")
                if media_type_to_agent is not None:
                    try:
                        media_type = media_type_to_agent(media_type)
                    except Exception:
                        pass
                status_ok = bool(getattr(record, "status", False))
                item = {
                    "index": index,
                    "id": getattr(record, "id", None),
                    "title": str(getattr(record, "title", "") or "未命名媒体"),
                    "year": str(getattr(record, "year", "") or ""),
                    "type": media_type,
                    "category": str(getattr(record, "category", "") or ""),
                    "season": str(getattr(record, "seasons", "") or ""),
                    "episode": str(getattr(record, "episodes", "") or ""),
                    "mode": str(getattr(record, "mode", "") or ""),
                    "status": "success" if status_ok else "failed",
                    "status_text": "成功" if status_ok else "失败",
                    "date": str(getattr(record, "date", "") or ""),
                    "downloader": str(getattr(record, "downloader", "") or ""),
                    "download_hash_short": str(getattr(record, "download_hash", "") or "")[:8],
                    "src_preview": self._path_preview(getattr(record, "src", "")),
                    "dest_preview": self._path_preview(getattr(record, "dest", "")),
                    "tmdbid": getattr(record, "tmdbid", None),
                    "doubanid": str(getattr(record, "doubanid", "") or ""),
                }
                errmsg = str(getattr(record, "errmsg", "") or "").strip()
                if errmsg and not status_ok:
                    item["errmsg"] = errmsg[:300]
                items.append(item)

            status_name = str(status or "all").strip().lower()
            status_label = "成功" if status_bool is True else "失败" if status_bool is False else "全部"
            title_label = f"：{title_text}" if title_text else ""
            if not items:
                return {
                    "success": True,
                    "message": f"未找到{status_label}整理历史{title_label}。",
                    "items": [],
                    "total": total,
                    "page": page_num,
                    "limit": page_size,
                    "status": status_name,
                }

            total_pages = (total + page_size - 1) // page_size if total else 1
            lines = [f"整理历史{title_label}：{status_label}，第 {page_num}/{total_pages} 页，共 {total} 条，展示 {len(items)} 条："]
            for item in items:
                season_episode = " ".join(value for value in [item.get("season"), item.get("episode")] if value)
                label_parts = [
                    item.get("status_text") or "-",
                    item.get("type") or "-",
                    item.get("mode") or "-",
                    item.get("date") or "-",
                ]
                lines.append(f"{item.get('index')}. {item.get('title')} ({item.get('year') or '-'}) {season_episode}".rstrip())
                lines.append("   " + " | ".join(label_parts))
                if item.get("dest_preview"):
                    lines.append(f"   目标：{item.get('dest_preview')}")
                if item.get("errmsg"):
                    lines.append(f"   错误：{item.get('errmsg')}")
            lines.append("说明：这是只读查询，用于判断下载后是否已经整理入库。")
            return {
                "success": True,
                "message": "\n".join(lines),
                "items": items,
                "total": total,
                "page": page_num,
                "limit": page_size,
                "status": status_name,
            }
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 查询整理历史失败：{exc}\n{traceback.format_exc()}")
            return {"success": False, "message": f"查询整理历史失败：{exc}", "items": []}

    def _execute_media_subscribe(self, keyword: str, immediate_search: bool) -> str:
        if not all([MetaInfo, SubscribeChain]):
            return "订阅失败：当前环境缺少 MoviePilot 订阅依赖。"
        meta = MetaInfo(keyword)
        try:
            sid, message = SubscribeChain().add(
                title=keyword,
                year=meta.year,
                mtype=meta.type,
                season=meta.begin_season,
                username="agentresourceofficer-feishu",
                exist_ok=True,
                message=False,
            )
            if not sid:
                return f"订阅失败：{keyword}\n原因：{message}"
            lines = [f"已创建订阅：{keyword}", f"订阅ID：{sid}", f"结果：{message}"]
            if immediate_search and Scheduler is not None:
                Scheduler().start(job_id="subscribe_search", **{"sid": sid, "state": None, "manual": True})
                lines.append("已触发一次订阅搜索。")
            return "\n".join(lines)
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 订阅失败：{keyword} {exc}\n{traceback.format_exc()}")
            return f"订阅失败：{keyword}\n错误：{exc}"

    @staticmethod
    def _format_media_label(mediainfo: Any, season: Optional[int] = None) -> str:
        title = getattr(mediainfo, "title", "") or "未知媒体"
        year = getattr(mediainfo, "year", None)
        label = f"{title} ({year})" if year else title
        media_type = getattr(mediainfo, "type", None)
        media_type_name = getattr(media_type, "name", "")
        if media_type_name == "TV" and season:
            return f"{label} 第{season}季"
        return label

    def _set_search_cache(self, cache_key: str, keyword: str, mediainfo: Any, results: List[Any]) -> None:
        with self._search_cache_lock:
            now = time.time()
            expired_keys = [
                key
                for key, item in self._search_cache.items()
                if now - float((item or {}).get("ts") or 0) > 1800
            ]
            for key in expired_keys:
                self._search_cache.pop(key, None)
            while len(self._search_cache) >= self._search_cache_limit:
                oldest_key = min(
                    self._search_cache,
                    key=lambda key: float((self._search_cache.get(key) or {}).get("ts") or 0),
                )
                self._search_cache.pop(oldest_key, None)
            self._search_cache[cache_key] = {
                "ts": now,
                "keyword": keyword,
                "mediainfo": mediainfo,
                "results": list(results or []),
            }

    def _get_search_cache(self, cache_key: str) -> Optional[Dict[str, Any]]:
        with self._search_cache_lock:
            item = self._search_cache.get(cache_key)
            if not item:
                return None
            if time.time() - float(item.get("ts") or 0) > 1800:
                self._search_cache.pop(cache_key, None)
                return None
            return item

    def _run_p115_manual_transfer_batch(self, paths: List[str], chat_id: str, open_id: str) -> None:
        summaries = [self._execute_p115_manual_transfer(path) for path in paths]
        self.reply_text(chat_id, open_id, "\n\n".join(item for item in summaries if item))

    def _run_p115_manual_transfer(self, path: str, chat_id: str, open_id: str) -> None:
        self.reply_text(chat_id, open_id, self._execute_p115_manual_transfer(path))

    def _get_p115_manual_transfer_paths(self) -> List[str]:
        try:
            config = self.plugin.systemconfig.get("plugin.P115StrmHelper") or {}
            raw = str(config.get("pan_transfer_paths") or "").strip()
            return [line.strip() for line in raw.splitlines() if line.strip()]
        except Exception as exc:
            logger.warning(f"[AgentResourceOfficer][Feishu] 获取待整理目录失败：{exc}")
            return []

    def _execute_p115_manual_transfer(self, path: str) -> str:
        log_path = Path("/config/logs/plugins/P115StrmHelper.log")
        log_offset = self._safe_log_offset(log_path)
        try:
            service_module = importlib.import_module("app.plugins.p115strmhelper.service")
            servicer = getattr(service_module, "servicer", None)
            if not servicer or not getattr(servicer, "monitorlife", None):
                return "刮削失败：P115StrmHelper 未初始化或未启用。"
            result = servicer.monitorlife.once_transfer(path)
            summary = self._format_p115_manual_transfer_result(result)
            return summary or self._build_p115_manual_transfer_summary(log_path, log_offset, path) or f"刮削完成：{path}"
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 手动刮削失败：{path} {exc}\n{traceback.format_exc()}")
            return f"刮削失败：{path}\n错误：{exc}"

    def _format_p115_manual_transfer_result(self, result: Any) -> Optional[str]:
        if not isinstance(result, dict):
            return None
        path = result.get("path") or ""
        failed_items = result.get("failed_items") or []
        lines = [
            f"刮削完成：{path}",
            f"总计：{result.get('total', 0)} 个项目（文件 {result.get('files', 0)}，文件夹 {result.get('dirs', 0)}）",
            f"成功：{result.get('success', 0)} 个",
            f"失败：{result.get('failed', 0)} 个",
            f"跳过：{result.get('skipped', 0)} 个",
        ]
        if result.get("error"):
            lines.append(f"错误：{result.get('error')}")
        if failed_items:
            lines.append("失败示例：")
            lines.extend(f"- {item}" for item in failed_items[:3])
            if len(failed_items) > 3:
                lines.append(f"- 还有 {len(failed_items) - 3} 项未展示")
        lines.extend(self._p115_strm_followup_lines(path))
        return "\n".join(lines)

    def _p115_strm_followup_lines(self, path: str) -> List[str]:
        hint = self._get_p115_strm_hint_path() or path
        return [
            "如需增量生成 STRM，请再发送：生成STRM",
            "如需按全部媒体库全量生成，请再发送：全量STRM",
            f"如需指定路径全量生成，请再发送：指定路径STRM {hint}",
        ]

    def _get_p115_strm_hint_path(self) -> Optional[str]:
        try:
            config = self.plugin.systemconfig.get("plugin.P115StrmHelper") or {}
            paths = str(config.get("full_sync_strm_paths") or "").strip()
            first_line = next((line.strip() for line in paths.splitlines() if line.strip()), "")
            if not first_line:
                return None
            parts = first_line.split("#")
            return parts[1].strip() if len(parts) >= 2 and parts[1].strip() else None
        except Exception:
            return None

    @staticmethod
    def _safe_log_offset(log_path: Path) -> int:
        try:
            return log_path.stat().st_size if log_path.exists() else 0
        except Exception:
            return 0

    def _build_p115_manual_transfer_summary(self, log_path: Path, start_offset: int, path: str) -> Optional[str]:
        try:
            if not log_path.exists():
                return None
            with log_path.open("r", encoding="utf-8", errors="ignore") as f:
                f.seek(start_offset)
                chunk = f.read()
            if not chunk:
                return None
            path_re = re.escape(path)
            pattern = re.compile(
                rf"手动网盘整理完成 - 路径: {path_re}\n"
                rf"\s*总计: (?P<total>\d+) 个项目 \(文件: (?P<files>\d+), 文件夹: (?P<dirs>\d+)\)\n"
                rf"\s*成功: (?P<success>\d+) 个\n"
                rf"\s*失败: (?P<failed>\d+) 个\n"
                rf"\s*跳过: (?P<skipped>\d+) 个",
                re.S,
            )
            match = pattern.search(chunk)
            if not match:
                return None
            summary = (
                f"刮削完成：{path}\n"
                f"总计：{match.group('total')} 个项目（文件 {match.group('files')}，文件夹 {match.group('dirs')}）\n"
                f"成功：{match.group('success')} 个\n"
                f"失败：{match.group('failed')} 个\n"
                f"跳过：{match.group('skipped')} 个"
            )
            return summary + "\n" + "\n".join(self._p115_strm_followup_lines(path))
        except Exception:
            return None

    def _submit_p115_command(self, command_text: str, chat_id: str, open_id: str) -> None:
        if PluginManager is not None:
            try:
                if not PluginManager().running_plugins.get("P115StrmHelper"):
                    self.reply_text(chat_id, open_id, "P115StrmHelper 未加载或未启用，无法执行 STRM 命令。")
                    return
            except Exception:
                pass
        self._submit_moviepilot_command(command_text, chat_id, open_id)

    def _submit_moviepilot_command(self, command_text: str, chat_id: str, open_id: str) -> None:
        if eventmanager is None or EventType is None:
            self.reply_text(chat_id, open_id, "当前环境缺少 MoviePilot 事件总线，无法转发该命令。")
            return
        eventmanager.send_event(
            EventType.CommandExcute,
            {"cmd": command_text, "source": None, "user": open_id or chat_id or "feishu"},
        )
        self.reply_text(chat_id, open_id, f"已接收命令：{command_text}\n任务已提交给 MoviePilot。")

    def _map_text_to_command(self, text: str) -> Optional[str]:
        text = self._sanitize_text(text)
        if not text:
            return None
        if text.startswith("/"):
            return text
        normalized = text.strip().lower()
        if normalized in {"n", "next", "下一页", "下页"} or normalized.startswith("n "):
            return f"/smart_pick {text}".strip()
        shortcut_match = re.fullmatch(r"(\d+)(?:\s+(.+))?", text)
        if shortcut_match:
            rest = str(shortcut_match.group(2) or "").strip()
            if not rest or "=" in rest or rest.startswith("/"):
                return f"/smart_pick {text}".strip()
        first_url = self.plugin._extract_first_url(text)
        if first_url and (self.plugin._is_115_url(first_url) or self.plugin._is_quark_url(first_url)):
            return f"/smart_entry {text}".strip()

        alias_map = self.parse_alias_text(self.command_aliases)
        parts = text.split(maxsplit=1)
        alias = parts[0]
        rest = parts[1] if len(parts) > 1 else ""
        target = alias_map.get(alias)
        if not target:
            for alias_key in sorted(alias_map.keys(), key=len, reverse=True):
                if not text.startswith(alias_key):
                    continue
                remain = text[len(alias_key):].strip()
                target = alias_map.get(alias_key)
                if target:
                    if target == "/smart_pick" and alias_key in {"详情", "审查"}:
                        return f"{target} {alias_key} {remain}".strip()
                    return f"{target} {remain}".strip()
            return None
        if target == "/smart_pick" and alias in {"详情", "审查"}:
            return f"{target} {alias} {rest}".strip()
        return f"{target} {rest}".strip()

    def _is_duplicate_event(self, event_id: str) -> bool:
        now = time.time()
        with self._event_lock:
            expired = [key for key, ts in self._event_cache.items() if now - ts > 600]
            for key in expired:
                self._event_cache.pop(key, None)
            if event_id in self._event_cache:
                return True
            self._event_cache[event_id] = now
        return self._is_duplicate_event_cross_instance(event_id, now)

    @staticmethod
    def _is_duplicate_event_cross_instance(event_id: str, now: float) -> bool:
        try:
            _EVENT_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
            with _EVENT_CACHE_FILE.open("a+", encoding="utf-8") as f:
                fcntl.flock(f.fileno(), fcntl.LOCK_EX)
                f.seek(0)
                raw = f.read().strip()
                cache = json.loads(raw) if raw else {}
                cache = {key: ts for key, ts in cache.items() if isinstance(ts, (int, float)) and now - float(ts) <= 600}
                if event_id in cache:
                    f.seek(0)
                    f.truncate()
                    json.dump(cache, f, ensure_ascii=False)
                    f.flush()
                    fcntl.flock(f.fileno(), fcntl.LOCK_UN)
                    return True
                cache[event_id] = now
                f.seek(0)
                f.truncate()
                json.dump(cache, f, ensure_ascii=False)
                f.flush()
                fcntl.flock(f.fileno(), fcntl.LOCK_UN)
        except Exception as exc:
            logger.warning(f"[AgentResourceOfficer][Feishu] 跨实例事件去重失败：{exc}")
        return False

    def _is_allowed(self, chat_id: str, user_open_id: str) -> bool:
        return bool(
            self.allow_all
            or (chat_id and chat_id in self.allowed_chat_ids)
            or (user_open_id and user_open_id in self.allowed_user_ids)
        )

    @staticmethod
    def _extract_text(content: Any) -> str:
        if isinstance(content, dict):
            return str(content.get("text") or "").strip()
        if isinstance(content, str):
            try:
                payload = json.loads(content)
            except json.JSONDecodeError:
                return content.strip()
            return str(payload.get("text") or "").strip()
        return ""

    @staticmethod
    def _sanitize_text(text: str) -> str:
        text = re.sub(r"<at[^>]*>.*?</at>", " ", text or "", flags=re.IGNORECASE)
        return re.sub(r"\s+", " ", text).strip()

    @staticmethod
    def _is_help_request(text: str) -> bool:
        return FeishuChannel._sanitize_text(text) in {"帮助", "/help", "help"}

    @staticmethod
    def _is_menu_request(text: str) -> bool:
        return FeishuChannel._sanitize_text(text) in {"菜单", "/menu", "menu", "面板", "控制面板"}

    def _build_help_text(self) -> str:
        aliases = self.parse_alias_text(self.command_aliases)
        alias_text = "\n".join(f"{key} -> {value}" for key, value in aliases.items()) or "未配置别名"
        return (
            "可用命令：\n"
            f"{', '.join(self.command_whitelist)}\n\n"
            "别名：\n"
            f"{alias_text}\n\n"
            "快捷入口：发送“菜单”可查看可复制的快捷命令。"
        )

    @staticmethod
    def _build_menu_text() -> str:
        return (
            "快捷菜单\n"
            "1. 盘搜搜索 片名\n"
            "2. 影巢搜索 片名\n"
            "3. 搜索 片名\n"
            "4. MP搜索 片名 / PT搜索 片名\n"
            "5. 转存 片名（默认 115）\n"
            "6. 夸克转存 片名\n"
            "7. 下载 片名\n"
            "8. 更新检查 片名\n"
            "9. 选择 序号 / 详情 序号 / n\n"
            "10. 115登录 / 115状态 / 115任务\n"
            "11. 影巢签到 / 影巢签到日志"
        )

    @staticmethod
    def _cache_key(chat_id: str, open_id: str) -> str:
        return f"feishu::{chat_id or ''}::{open_id or ''}"

    @staticmethod
    def _brief_response_error(data: Any) -> str:
        if not isinstance(data, dict):
            return "body=<non-json>"
        code = str(data.get("code") or "").strip()
        msg = str(data.get("msg") or data.get("message") or "").strip()
        parts: List[str] = []
        if code:
            parts.append(f"code={code}")
        if msg:
            parts.append(f"msg={msg}")
        return " ".join(parts) if parts else "body=<json>"

    def reply_text(self, chat_id: str, open_id: str, text: str) -> None:
        if not self.reply_enabled or not self.app_id or not self.app_secret:
            return
        receive_id = chat_id if self.reply_receive_id_type == "chat_id" else open_id
        if not receive_id:
            return
        access_token = self._get_tenant_access_token()
        if not access_token or RequestUtils is None:
            return
        url = f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={self.reply_receive_id_type}"
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json; charset=utf-8",
        }
        payload = {
            "receive_id": receive_id,
            "msg_type": "text",
            "content": json.dumps({"text": text}, ensure_ascii=False),
        }
        response = RequestUtils(headers=headers).post(url=url, json=payload)
        if response is None:
            logger.error("[AgentResourceOfficer][Feishu] 发送文本失败：无响应")
            return
        try:
            data = response.json()
        except Exception:
            data = {}
        if response.status_code != 200 or data.get("code") not in (0, None):
            logger.error(
                f"[AgentResourceOfficer][Feishu] 发送文本失败: status={response.status_code} "
                f"{self._brief_response_error(data)}"
            )

    def reply_qrcode_data_url(self, chat_id: str, open_id: str, data_url: str) -> None:
        text = str(data_url or "").strip()
        if not text.startswith("data:image/") or ";base64," not in text:
            return
        _, _, payload = text.partition(";base64,")
        try:
            image_bytes = b64decode(payload)
        except Exception as exc:
            logger.error(f"[AgentResourceOfficer][Feishu] 解码二维码失败：{exc}")
            return
        image_key = self._upload_image(image_bytes=image_bytes, file_name="p115-qrcode.png")
        if image_key:
            self._reply_image(chat_id, open_id, image_key)

    def _upload_image(self, image_bytes: bytes, file_name: str) -> Optional[str]:
        if not image_bytes or RequestUtils is None:
            return None
        access_token = self._get_tenant_access_token()
        if not access_token:
            return None
        response = RequestUtils(headers={"Authorization": f"Bearer {access_token}"}).post(
            url="https://open.feishu.cn/open-apis/im/v1/images",
            data={"image_type": "message"},
            files={"image": (file_name, image_bytes, "image/png")},
        )
        if response is None:
            logger.error("[AgentResourceOfficer][Feishu] 上传图片失败：无响应")
            return None
        try:
            data = response.json()
        except Exception:
            data = {}
        if response.status_code != 200 or data.get("code") not in (0, None):
            logger.error(
                f"[AgentResourceOfficer][Feishu] 上传图片失败: status={response.status_code} "
                f"{self._brief_response_error(data)}"
            )
            return None
        return str(((data.get("data") or {}).get("image_key")) or "").strip() or None

    def _reply_image(self, chat_id: str, open_id: str, image_key: str) -> None:
        if not image_key or RequestUtils is None:
            return
        receive_id = chat_id if self.reply_receive_id_type == "chat_id" else open_id
        if not receive_id:
            return
        access_token = self._get_tenant_access_token()
        if not access_token:
            return
        url = f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={self.reply_receive_id_type}"
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json; charset=utf-8",
        }
        payload = {
            "receive_id": receive_id,
            "msg_type": "image",
            "content": json.dumps({"image_key": image_key}, ensure_ascii=False),
        }
        response = RequestUtils(headers=headers).post(url=url, json=payload)
        if response is None:
            logger.error("[AgentResourceOfficer][Feishu] 发送图片失败：无响应")
            return
        try:
            data = response.json()
        except Exception:
            data = {}
        if response.status_code != 200 or data.get("code") not in (0, None):
            logger.error(
                f"[AgentResourceOfficer][Feishu] 发送图片失败: status={response.status_code} "
                f"{self._brief_response_error(data)}"
            )

    def _get_tenant_access_token(self) -> Optional[str]:
        if RequestUtils is None:
            return None
        now = time.time()
        with self._token_lock:
            token = self._token_cache.get("token")
            expires_at = float(self._token_cache.get("expires_at") or 0)
            if token and now < expires_at - 60:
                return token
            response = RequestUtils(content_type="application/json").post(
                url="https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/",
                json={"app_id": self.app_id, "app_secret": self.app_secret},
            )
            if response is None:
                logger.error("[AgentResourceOfficer][Feishu] 获取 tenant_access_token 失败：无响应")
                return None
            try:
                data = response.json()
            except Exception as exc:
                logger.error(f"[AgentResourceOfficer][Feishu] token 响应解析失败：{exc}")
                return None
            token = data.get("tenant_access_token")
            expire = int(data.get("expire") or 0)
            if not token:
                logger.error(
                    f"[AgentResourceOfficer][Feishu] token 缺失：{self._brief_response_error(data)}"
                )
                return None
            self._token_cache = {"token": token, "expires_at": now + expire}
            return token
