{{--
    文件功能：聊天室核心前端交互脚本（Blade 模板形式）

    包含：
    1. 消息渲染与路由（appendMessage）
    2. 在线用户列表管理（renderUserList / filterUserList）
    3. WebSocket 事件监听（chat:here / chat:message / chat:muted 等）
    4. 管理操作（adminClearScreen / promptAnnouncement 等）
    5. 存点心跳（saveExp，60秒自动）
    6. 钓鱼小游戏（startFishing / reelFish / autoFish）
    7. 发送消息（sendMessage，IME 防重触发）
    8. 特效控制（triggerEffect / applyFontSize / toggleSoundMute）
    9. 系统播报屏蔽（toggleBlockMenu / toggleBlockedSystemSender）

    已拆分至独立文件：
    - window.chatBanner     → chat-banner.blade.php
    - 头像选择器 JS         → layout/toolbar.blade.php
    - 好友通知/chatBanner监听 → user-actions.blade.php
    - 红包 HTML+CSS+JS      → games/red-packet-panel.blade.php

    通过 @include('chat.partials.scripts') 引入到 frame.blade.php

    @author  ChatRoom Laravel
    @version 2.0.0
--}}

<script>
    /**
     * 聊天室前端交互逻辑
     * 保留所有 WebSocket 事件监听，复刻原版 UI 交互
     */

    // ── DOM 元素引用 ──────────────────────────────────────
    const container = document.getElementById('chat-messages-container');
    const container2 = document.getElementById('chat-messages-container2');
    const userList = document.getElementById('online-users-list');
    const toUserSelect = document.getElementById('to_user');
    const onlineCount = document.getElementById('online-count');
    const onlineCountBottom = document.getElementById('online-count-bottom');
    const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = window.ChatRoomTools?.BLOCKED_SYSTEM_SENDERS_STORAGE_KEY || 'chat_blocked_system_senders';
    const CHAT_SOUND_MUTED_STORAGE_KEY = window.ChatRoomTools?.CHAT_SOUND_MUTED_STORAGE_KEY || 'chat_sound_muted';
    const BLOCKABLE_SYSTEM_SENDERS = window.ChatRoomTools?.BLOCKABLE_SYSTEM_SENDERS || ['钓鱼播报', '星海小博士', '百家乐', '跑马', '神秘箱子'];
    const hoverTooltip = document.getElementById('chat-hover-tooltip');
    let activeTooltipTrigger = null;

    // ── 消息区：手机端双触发打开用户名片（PC 端靠 ondblclick 内联属性）──
    // span[data-u] 由 clickableUser() 生成，touchend 委托至容器避免每条消息单独绑定
    (function _bindMsgDoubleTap() {
        let _lastTapTarget = null;
        let _lastTapTime   = 0;

        /** touchend 委托处理函数 */
        function _onMsgTouch(e) {
            const span = e.target.closest('[data-u]');
            if (!span) return;
            const now = Date.now();
            if (span === _lastTapTarget && now - _lastTapTime < 300) {
                e.preventDefault();
                openUserCard(span.dataset.u);
                _lastTapTarget = null;
                _lastTapTime   = 0;
            } else {
                _lastTapTarget = span;
                _lastTapTime   = now;
            }
        }

        // 两个聊天容器（公屏 + 包厢）都绑定
        [container, container2].forEach(c => {
            if (c) { c.addEventListener('touchend', _onMsgTouch, { passive: false }); }
        });
    })();
    let onlineUsers = {};
    // 暂时暴露给已迁移的手机抽屉 Vite 模块读取，后续在线名单整体迁移后可移除。
    window.onlineUsers = onlineUsers;
    let autoScroll = true;
    let userBadgeRotationTick = 0;
    let userListRenderTimer = null;
    let _maxMsgId = 0; // 记录当前收到的最大消息 ID
    let pendingChatMessages = [];
    let chatMessageFlushTimer = null;
    let userFilterRenderTimer = null;
    let lastAutosaveNode = null;
    const CHAT_MESSAGE_FLUSH_BATCH_SIZE = 8;
    const PUBLIC_MESSAGE_NODE_LIMIT = 600;
    const PRIVATE_MESSAGE_NODE_LIMIT = 300;
    const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {});
    let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);

    /**
     * 规整聊天室偏好对象，过滤非法配置并补齐默认值。
     *
     * @param {Record<string, any>} raw 原始偏好对象
     * @returns {Object}
     */
    function normalizeChatPreferences(raw) {
        if (window.ChatRoomTools?.normalizeChatPreferences) {
            return window.ChatRoomTools.normalizeChatPreferences(raw, BLOCKABLE_SYSTEM_SENDERS);
        }

        const blocked = Array.isArray(raw?.blocked_system_senders)
            ? raw.blocked_system_senders.filter(sender => BLOCKABLE_SYSTEM_SENDERS.includes(sender))
            : [];

        return {
            // 默认所有用户都处于“不屏蔽”的开放状态，只有显式勾选的项目才会进入该列表。
            blocked_system_senders: Array.from(new Set(blocked)),
            sound_muted: Boolean(raw?.sound_muted),
        };
    }

    /**
     * 解析并标准化状态到期时间。
     *
     * @param {string|null|undefined} expiresAt 原始到期时间
     * @returns {Date|null}
     */
    function parseDailyStatusExpiry(expiresAt) {
        if (window.ChatRoomTools?.parseDailyStatusExpiry) {
            return window.ChatRoomTools.parseDailyStatusExpiry(expiresAt);
        }

        if (!expiresAt) {
            return null;
        }

        const parsed = new Date(expiresAt);

        return Number.isNaN(parsed.getTime()) ? null : parsed;
    }

    /**
     * 将状态对象规整为前端统一结构，并过滤掉已过期状态。
     *
     * @param {Record<string, any>|null|undefined} raw 原始状态对象
     * @returns {Object|null}
     */
    function normalizeDailyStatus(raw) {
        if (window.ChatRoomTools?.normalizeDailyStatus) {
            return window.ChatRoomTools.normalizeDailyStatus(raw);
        }

        if (!raw || typeof raw !== 'object') {
            return null;
        }

        const key = String(raw.key ?? raw.daily_status_key ?? '');
        const label = String(raw.label ?? raw.daily_status_label ?? '');
        const icon = String(raw.icon ?? raw.daily_status_icon ?? '');
        const group = String(raw.group ?? raw.daily_status_group ?? '');
        const expiresAt = raw.expires_at ?? raw.daily_status_expires_at ?? null;
        const parsedExpiry = parseDailyStatusExpiry(expiresAt);

        if (!key || !label || !icon || !parsedExpiry) {
            return null;
        }

        if (parsedExpiry.getTime() <= Date.now()) {
            return null;
        }

        return {
            key,
            label,
            icon,
            group,
            expires_at: parsedExpiry.toISOString(),
        };
    }

    /**
     * 获取当前登录用户仍然有效的状态。
     *
     * @returns {Object|null}
     */
    function getCurrentUserDailyStatus() {
        return normalizeDailyStatus(window.chatContext?.currentDailyStatus);
    }

    /**
     * 清除用户在线载荷中的状态字段，避免合并时残留旧状态。
     *
     * @param {Record<string, any>} payload 用户在线载荷
     */
    function removeDailyStatusFields(payload) {
        if (!payload || typeof payload !== 'object') {
            return;
        }

        delete payload.daily_status_key;
        delete payload.daily_status_label;
        delete payload.daily_status_icon;
        delete payload.daily_status_group;
        delete payload.daily_status_expires_at;
    }

    /**
     * 将状态写回指定用户的在线载荷。
     *
     * @param {string} username 用户名
     * @param {Object|null} status 标准化后的状态对象
     */
    function setOnlineUserDailyStatus(username, status) {
        if (!username || !onlineUsers[username]) {
            return;
        }

        removeDailyStatusFields(onlineUsers[username]);

        if (!status) {
            return;
        }

        onlineUsers[username].daily_status_key = status.key;
        onlineUsers[username].daily_status_label = status.label;
        onlineUsers[username].daily_status_icon = status.icon;
        onlineUsers[username].daily_status_group = status.group;
        onlineUsers[username].daily_status_expires_at = status.expires_at;
    }

    /**
     * 用服务端最新的在线载荷刷新指定用户，并先清空旧状态字段。
     *
     * @param {string} username 用户名
     * @param {Record<string, any>} payload 最新在线载荷
     */
    function hydrateOnlineUserPayload(username, payload) {
        const nextPayload = {
            ...(onlineUsers[username] || {}),
        };

        removeDailyStatusFields(nextPayload);
        onlineUsers[username] = {
            ...nextPayload,
            ...payload,
        };
    }

    /**
     * 同步状态按钮文字与图标。
     */
    function syncDailyStatusTrigger() {
        const shortcutIcon = document.getElementById('daily-status-shortcut-icon');
        const shortcutLabel = document.getElementById('daily-status-shortcut-label');
        const activeStatus = getCurrentUserDailyStatus();

        if (shortcutIcon) {
            shortcutIcon.textContent = activeStatus?.icon || '🙂';
        }

        if (shortcutLabel) {
            shortcutLabel.textContent = activeStatus?.label || '状态';
        }
    }

    /**
     * 同步状态面板中当前选中项的高亮样式。
     */
    function syncDailyStatusMenuSelection() {
        const activeKey = getCurrentUserDailyStatus()?.key || '';

        document.querySelectorAll('#daily-status-editor-overlay .daily-status-item').forEach((button) => {
            const selected = button.dataset.statusKey === activeKey;
            button.style.borderColor = selected ? '#6366f1' : '#e5e7eb';
            button.style.background = selected ? 'linear-gradient(180deg,#eef2ff 0%,#e0e7ff 100%)' : '#ffffffcc';
            button.style.color = selected ? '#312e81' : '#334155';
            button.style.boxShadow = selected ? '0 8px 18px rgba(99,102,241,.18)' : 'none';
            button.style.transform = selected ? 'translateY(-1px)' : 'translateY(0)';
        });
    }

    /**
     * 同步聊天室状态相关 UI。
     */
    function syncDailyStatusUi() {
        const activeStatus = getCurrentUserDailyStatus();

        if (window.chatContext) {
            window.chatContext.currentDailyStatus = activeStatus;
        }

        syncDailyStatusTrigger();
        syncDailyStatusMenuSelection();
    }

    /**
     * 关闭功能菜单。
     */
    function closeFeatureMenu() {
        const menu = document.getElementById('feature-menu');

        if (menu) {
            menu.style.display = 'none';
        }
    }

    /**
     * 切换功能菜单显示状态。
     *
     * @param {Event} event 点击事件
     */
    function toggleFeatureMenu(event) {
        event.stopPropagation();

        const menu = document.getElementById('feature-menu');
        const welcomeMenu = document.getElementById('welcome-menu');
        const adminMenu = document.getElementById('admin-menu');
        const blockMenu = document.getElementById('block-menu');
        const editorOverlay = document.getElementById('daily-status-editor-overlay');

        if (!menu) {
            return;
        }

        if (welcomeMenu) {
            welcomeMenu.style.display = 'none';
        }

        if (adminMenu) {
            adminMenu.style.display = 'none';
        }

        if (blockMenu) {
            blockMenu.style.display = 'none';
        }

        if (editorOverlay) {
            editorOverlay.style.display = 'none';
        }

        syncDailyStatusUi();
        menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
    }

    /**
     * 打开状态编辑窗口。
     */
    function openDailyStatusEditor() {
        const overlay = document.getElementById('daily-status-editor-overlay');

        closeFeatureMenu();
        syncDailyStatusUi();

        if (overlay) {
            overlay.style.display = 'block';
        }
    }

    /**
     * 关闭状态编辑窗口。
     */
    function closeDailyStatusEditor() {
        const overlay = document.getElementById('daily-status-editor-overlay');

        if (overlay) {
            overlay.style.display = 'none';
        }
    }

    /**
     * 提交状态设置/清除请求。
     *
     * @param {Object} payload 请求载荷
     * @returns {Promise<Object>}
     */
    async function submitDailyStatusPayload(payload) {
        const response = await fetch(window.chatContext.dailyStatusUpdateUrl, {
            method: 'PUT',
            headers: {
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            body: JSON.stringify(payload),
        });
        const result = await response.json();

        if (!response.ok || result?.status !== 'success') {
            throw new Error(result?.message || '状态保存失败');
        }

        return result;
    }

    /**
     * 将服务端返回的状态结果应用到当前用户本地名单。
     *
     * @param {Object|null} status 标准化后的状态对象
     */
    function applyCurrentUserDailyStatus(status) {
        if (window.chatContext) {
            window.chatContext.currentDailyStatus = status;
        }

        setOnlineUserDailyStatus(window.chatContext.username, status);
        syncDailyStatusUi();
        renderUserList();
    }

    /**
     * 从服务端响应中提取最新金币余额。
     *
     * @param {Record<string, any>} data 接口响应数据
     * @returns {number|null}
     */
    function resolveDailySignInGoldBalance(data) {
        const candidates = [
            data?.data?.user?.jjb,
            data?.data?.user?.gold,
            data?.data?.presence?.jjb,
            data?.data?.presence?.gold,
            data?.data?.my_jjb,
            data?.data?.new_jjb,
            data?.data?.balance,
            data?.my_jjb,
            data?.new_jjb,
            data?.balance,
        ];

        for (const candidate of candidates) {
            const amount = Number(candidate);

            if (Number.isFinite(amount)) {
                return amount;
            }
        }

        return null;
    }

    /**
     * 从签到响应中提取当前用户最新在线载荷。
     *
     * @param {Record<string, any>} data 接口响应数据
     * @returns {Record<string, any>|null}
     */
    function resolveDailySignInPresencePayload(data) {
        const candidates = [
            data?.data?.presence,
            data?.data?.online_user,
            data?.data?.onlineUser,
            data?.data?.user_payload,
            data?.data?.userPayload,
            data?.data?.user,
            data?.presence,
            data?.online_user,
            data?.onlineUser,
        ];

        return candidates.find(payload => payload && typeof payload === 'object') || null;
    }

    /**
     * 从签到响应中提取签到身份字段。
     *
     * @param {Record<string, any>} data 接口响应数据
     * @returns {Record<string, any>}
     */
    function resolveDailySignInIdentityPayload(data) {
        const identity = data?.data?.identity || data?.data?.sign_identity || data?.identity || data?.sign_identity;

        if (!identity || typeof identity !== 'object') {
            return {};
        }

        return {
            sign_identity_key: identity.key ?? identity.sign_identity_key ?? identity.code ?? '',
            sign_identity_label: identity.label ?? identity.name ?? identity.sign_identity_label ?? '',
            sign_identity_icon: identity.icon ?? identity.sign_identity_icon ?? '',
            sign_identity_color: identity.color ?? identity.sign_identity_color ?? undefined,
            sign_identity_bg_color: identity.bg_color ?? identity.background_color ?? identity.sign_identity_bg_color ?? undefined,
            sign_identity_border_color: identity.border_color ?? identity.sign_identity_border_color ?? undefined,
        };
    }

    /**
     * 将签到成功结果同步到金币余额与在线名单。
     *
     * @param {Record<string, any>} data 接口响应数据
     */
    function applyDailySignInResult(data) {
        const balance = resolveDailySignInGoldBalance(data);
        const payload = resolveDailySignInPresencePayload(data);
        const identityPayload = resolveDailySignInIdentityPayload(data);
        const username = window.chatContext?.username;

        if (balance !== null && window.chatContext) {
            window.chatContext.userJjb = balance;
            window.chatContext.myGold = balance;
        }

        if (username) {
            hydrateOnlineUserPayload(username, {
                ...(payload || {}),
                ...identityPayload,
                username,
            });
        }

        renderUserList();
    }

    window.dailySignInState = {
        month: null,
        prevMonth: null,
        nextMonth: null,
        repairCardItem: null,
        repairCardCount: 0,
        rewardRules: [],
        status: null,
    };

    /**
     * 打开每日签到日历弹窗。
     */
    window.openDailySignInModal = async function openDailySignInModal() {
        const modal = document.getElementById('daily-sign-modal');

        if (!window.chatContext?.dailySignInCalendarUrl || !modal) {
            window.chatDialog?.alert('签到入口暂未开放，请稍后再试。', '提示', '#f59e0b');
            return;
        }

        modal.style.display = 'flex';
        await Promise.all([
            loadDailySignInStatus(),
            loadDailySignInCalendar(window.dailySignInState.month),
        ]);
    };

    /**
     * 关闭每日签到日历弹窗。
     */
    window.closeDailySignInModal = function closeDailySignInModal() {
        const modal = document.getElementById('daily-sign-modal');

        if (modal) {
            modal.style.display = 'none';
        }
    };

    /**
     * 快速签到入口：打开日历，让用户能看到当月签到和补签状态。
     */
    window.quickDailySignIn = async function quickDailySignIn() {
        await window.openDailySignInModal();
    };

    /**
     * 拉取今日签到状态。
     */
    async function loadDailySignInStatus() {
        const statusUrl = window.chatContext?.dailySignInStatusUrl;

        if (!statusUrl) {
            return;
        }

        const response = await fetch(statusUrl, {
            headers: {
                'Accept': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
            },
        });
        const data = await response.json();

        if (!response.ok || data?.status === 'error') {
            throw new Error(data?.message || '签到状态加载失败');
        }

        window.dailySignInState.status = data.data || {};
        renderDailySignInStatus();
    }

    /**
     * 拉取并渲染指定月份签到日历。
     *
     * @param {string|null|undefined} month 月份 YYYY-MM
     */
    window.loadDailySignInCalendar = async function loadDailySignInCalendar(month) {
        const calendarUrl = window.chatContext?.dailySignInCalendarUrl;

        if (!calendarUrl) {
            return;
        }

        const url = new URL(calendarUrl, window.location.origin);
        if (month) {
            url.searchParams.set('month', month);
        }

        const response = await fetch(url.toString(), {
            headers: {
                'Accept': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
            },
        });
        const data = await response.json();

        if (!response.ok || data?.status === 'error') {
            throw new Error(data?.message || '签到日历加载失败');
        }

        const payload = data.data || {};
        window.dailySignInState.month = payload.month || month || null;
        window.dailySignInState.prevMonth = payload.prev_month || null;
        window.dailySignInState.nextMonth = payload.next_month || null;
        window.dailySignInState.repairCardItem = payload.sign_repair_card_item || null;
        window.dailySignInState.repairCardCount = Number(payload.makeup_card_count || 0);
        window.dailySignInState.rewardRules = Array.isArray(payload.reward_rules) ? payload.reward_rules : [];
        renderDailySignInCalendar(payload);
        renderDailySignInStatus();
        renderDailySignInRewardRules();
    };

    /**
     * 渲染签到状态摘要。
     */
    function renderDailySignInStatus() {
        const status = window.dailySignInState.status || {};
        const streakEl = document.getElementById('daily-sign-streak');
        const previewEl = document.getElementById('daily-sign-preview');
        const cardCountEl = document.getElementById('daily-sign-card-count');
        const cardPriceEl = document.getElementById('daily-sign-card-price');
        const claimBtn = document.getElementById('daily-sign-claim-btn');
        const buyBtn = document.getElementById('daily-sign-buy-card-btn');
        const cardItem = window.dailySignInState.repairCardItem;

        if (streakEl) {
            streakEl.textContent = `连续 ${Number(status.current_streak_days || 0)} 天`;
        }
        if (previewEl) {
            const rule = status.preview_rule || {};
            const parts = [];
            if (Number(rule.gold_reward || 0) > 0) parts.push(`${rule.gold_reward} 金币`);
            if (Number(rule.exp_reward || 0) > 0) parts.push(`${rule.exp_reward} 经验`);
            if (Number(rule.charm_reward || 0) > 0) parts.push(`${rule.charm_reward} 魅力`);
            previewEl.textContent = status.signed_today ? '今日已签到' : `今日可领：${parts.join(' + ') || '签到奖励'}`;
        }
        if (cardCountEl) {
            cardCountEl.textContent = `补签卡 ${window.dailySignInState.repairCardCount || 0} 张`;
        }
        if (cardPriceEl) {
            cardPriceEl.textContent = cardItem ? `${cardItem.icon || '🗓️'} ${cardItem.name}：${Number(cardItem.price || 0).toLocaleString()} 金币` : '补签卡暂未上架';
        }
        if (claimBtn) {
            claimBtn.disabled = !!status.signed_today;
            claimBtn.textContent = status.signed_today ? '今日已签到' : '今日签到';
            claimBtn.style.opacity = status.signed_today ? '0.55' : '1';
            claimBtn.style.cursor = status.signed_today ? 'not-allowed' : 'pointer';
        }
        if (buyBtn) {
            buyBtn.disabled = !cardItem?.id;
            buyBtn.style.opacity = cardItem?.id ? '1' : '0.55';
            buyBtn.style.cursor = cardItem?.id ? 'pointer' : 'not-allowed';
        }
    }

    /**
     * 渲染签到月历。
     *
     * @param {Record<string, any>} payload 日历响应数据
     */
    function renderDailySignInCalendar(payload) {
        const grid = document.getElementById('daily-sign-calendar-grid');
        const label = document.getElementById('daily-sign-month-label');

        if (!grid) {
            return;
        }

        if (label) {
            label.textContent = payload.month_label || payload.month || '本月';
        }

        const days = Array.isArray(payload.days) ? payload.days : [];
        grid.innerHTML = '';

        const firstWeekday = Number(days[0]?.weekday || 0);
        for (let i = 0; i < firstWeekday; i += 1) {
            const blank = document.createElement('div');
            blank.className = 'daily-sign-day blank';
            grid.appendChild(blank);
        }

        days.forEach(day => {
            const cell = document.createElement('button');
            cell.type = 'button';
            cell.className = 'daily-sign-day';
            if (day.signed) cell.classList.add('signed');
            if (day.can_makeup) cell.classList.add('missed');
            if (day.is_today) cell.classList.add('today');
            if (day.is_future) cell.classList.add('future');

            const stateText = day.signed
                ? `${day.is_makeup ? '补签' : '已签'} ${day.streak_days || ''}天`
                : (day.is_future ? '未到' : (day.is_today ? '今天' : '漏签'));

            cell.innerHTML = `<span class="day-num">${day.day}</span><span class="day-state">${escapeHtml(stateText)}</span>`;
            cell.title = day.reward_text || stateText;
            if (day.can_makeup) {
                cell.onclick = () => makeupDailySignIn(day.date);
            }
            grid.appendChild(cell);
        });
    }

    /**
     * 渲染连续签到奖励目标列表。
     */
    function renderDailySignInRewardRules() {
        const list = document.getElementById('daily-sign-rewards-list');
        const progress = document.getElementById('daily-sign-reward-progress');

        if (!list) {
            return;
        }

        const currentDays = Number(window.dailySignInState.status?.current_streak_days || 0);
        const rules = window.dailySignInState.rewardRules || [];

        if (progress) {
            progress.textContent = `当前 ${currentDays} 天`;
        }

        if (!rules.length) {
            list.innerHTML = '<div style="font-size:12px;color:#94a3b8;padding:4px;">暂无奖励规则</div>';
            return;
        }

        list.innerHTML = rules.map(rule => {
            const streakDays = Number(rule.streak_days || 0);
            const parts = [];
            if (Number(rule.gold_reward || 0) > 0) parts.push(`${rule.gold_reward}金`);
            if (Number(rule.exp_reward || 0) > 0) parts.push(`${rule.exp_reward}经验`);
            if (Number(rule.charm_reward || 0) > 0) parts.push(`${rule.charm_reward}魅力`);

            const icon = escapeHtml(rule.identity_badge_icon || '✅');
            const name = escapeHtml(rule.identity_badge_name || '签到奖励');
            const color = escapeHtml(rule.identity_badge_color || '#0f766e');
            const activeClass = currentDays >= streakDays ? ' active' : '';
            const distanceText = currentDays >= streakDays ? '已达成' : `还差 ${Math.max(streakDays - currentDays, 0)} 天`;
            const rewardText = escapeHtml(parts.join(' + ') || '签到记录');

            return `
                <div class="daily-sign-reward-card${activeClass}" title="${name} · ${rewardText} · ${escapeHtml(distanceText)}">
                    <div class="daily-sign-reward-title">
                        <span>第 ${streakDays} 天</span>
                        <span style="color:${color};">${icon}</span>
                    </div>
                    <div class="daily-sign-reward-name">${name}</div>
                    <div class="daily-sign-reward-desc">${rewardText}</div>
                </div>
            `;
        }).join('');
    }

    /**
     * 在日历弹窗中领取今日签到。
     */
    window.claimDailySignInFromModal = async function claimDailySignInFromModal() {
        const claimUrl = window.chatContext?.dailySignInClaimUrl;

        if (!claimUrl) {
            window.chatDialog?.alert('签到入口暂未开放，请稍后再试。', '提示', '#f59e0b');
            return;
        }

        try {
            const response = await fetch(claimUrl, {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                },
                body: JSON.stringify({
                    room_id: window.chatContext?.roomId ?? null,
                }),
            });
            const data = await response.json();

            if (!response.ok || data?.status === 'error' || data?.ok === false) {
                throw new Error(data?.message || '签到失败');
            }

            applyDailySignInResult(data);
            await Promise.all([
                loadDailySignInStatus(),
                loadDailySignInCalendar(window.dailySignInState.month),
            ]);
            renderDailySignInRewardRules();
            window.chatToast?.show({
                title: '签到成功',
                message: data?.message || '今日签到奖励已到账。',
                icon: data?.data?.sign_identity_icon || data?.data?.identity?.icon || '✅',
                color: '#16a34a',
                duration: 3200,
            });
        } catch (error) {
            window.chatDialog?.alert(error.message || '签到失败，请稍后重试。', '签到失败', '#cc4444');
        }
    };

    /**
     * 使用补签卡补签指定日期。
     *
     * @param {string} targetDate 目标日期 YYYY-MM-DD
     */
    async function makeupDailySignIn(targetDate) {
        const makeupUrl = window.chatContext?.dailySignInMakeupUrl;

        if (!makeupUrl) {
            return;
        }

        const ok = await window.chatDialog?.confirm(`确认使用 1 张补签卡补签 ${targetDate} 吗？`, '确认补签');
        if (!ok) {
            return;
        }

        try {
            const response = await fetch(makeupUrl, {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                },
                body: JSON.stringify({
                    target_date: targetDate,
                    room_id: window.chatContext?.roomId ?? null,
                }),
            });
            const data = await response.json();

            if (!response.ok || data?.status === 'error' || data?.ok === false) {
                const firstError = data?.errors ? Object.values(data.errors).flat()[0] : null;
                throw new Error(firstError || data?.message || '补签失败');
            }

            applyDailySignInResult(data);
            await Promise.all([
                loadDailySignInStatus(),
                loadDailySignInCalendar(window.dailySignInState.month),
            ]);
            renderDailySignInRewardRules();
            window.chatToast?.show({
                title: '补签成功',
                message: data?.message || '补签已完成。',
                icon: '🗓️',
                color: '#0f766e',
                duration: 3200,
            });
        } catch (error) {
            window.chatDialog?.alert(error.message || '补签失败，请稍后重试。', '补签失败', '#cc4444');
        }
    }

    /**
     * 询问补签卡购买数量。
     *
     * @param {Record<string, any>} item 补签卡商品
     * @returns {Promise<number|null>}
     */
    window.promptSignRepairQuantity = async function promptSignRepairQuantity(item) {
        const unitPrice = Number(item?.price || 0);
        const ruleText = item?.description || '补签卡只能补签本月漏掉的未签到日期，不能补签上月或更早日期。';
        const promptPromise = window.chatDialog?.prompt(
            `请输入要购买的补签卡数量（1-99）：\n单价 ${unitPrice.toLocaleString()} 金币\n说明：${ruleText}`,
            '1',
            '购买补签卡',
            '#0f766e'
        );
        const inputEl = document.getElementById('global-dialog-input');
        const previousInputStyle = inputEl?.getAttribute('style') || '';

        if (inputEl) {
            inputEl.style.minHeight = '40px';
            inputEl.style.height = '40px';
            inputEl.style.resize = 'none';
            inputEl.style.overflow = 'hidden';
        }

        const rawQuantity = await promptPromise;

        if (inputEl) {
            inputEl.setAttribute('style', previousInputStyle);
        }

        if (rawQuantity === null || rawQuantity === undefined) {
            return null;
        }

        const quantity = Number.parseInt(String(rawQuantity).trim(), 10);
        if (!Number.isInteger(quantity) || quantity < 1 || quantity > 99) {
            window.chatDialog?.alert('购买数量必须是 1 到 99 之间的整数。', '数量不正确', '#cc4444');
            return null;
        }

        return quantity;
    };

    /**
     * 在签到弹窗内快速购买补签卡。
     */
    window.buyDailySignRepairCard = async function buyDailySignRepairCard() {
        const item = window.dailySignInState.repairCardItem;

        if (!item?.id) {
            window.chatDialog?.alert('补签卡暂未上架。', '提示', '#f59e0b');
            return;
        }

        const quantity = await window.promptSignRepairQuantity(item);
        if (quantity === null) {
            return;
        }

        const totalPrice = Number(item.price || 0) * quantity;
        const ok = await window.chatDialog?.confirm(`确认花费 ${totalPrice.toLocaleString()} 金币购买【${item.name}】× ${quantity} 吗？\n说明：补签卡只能补签本月未签到日期。`, '购买补签卡');
        if (!ok) {
            return;
        }

        if (typeof window.buyItem === 'function') {
            window.buyItem(item.id, item.name, item.price, 'all', '', quantity);
            setTimeout(() => {
                loadDailySignInCalendar(window.dailySignInState.month);
                loadDailySignInStatus();
            }, 900);
            return;
        }

        window.openShopModal?.();
    };

    /**
     * 设置新的当日状态。
     *
     * @param {string} statusKey 状态键
     */
    async function updateDailyStatus(statusKey) {
        if (!window.chatContext?.dailyStatusUpdateUrl) {
            return;
        }

        try {
            const result = await submitDailyStatusPayload({
                room_id: window.chatContext.roomId,
                action: 'set',
                status_key: statusKey,
            });
            const status = normalizeDailyStatus(result?.data?.status);

            applyCurrentUserDailyStatus(status);
            closeDailyStatusEditor();
            window.chatToast?.show({
                title: '状态已更新',
                message: status ? `${status.icon} ${status.label}` : '已更新',
                icon: status?.icon || '🙂',
                color: '#4f46e5',
                duration: 2600,
            });
        } catch (error) {
            window.chatDialog?.alert(error.message || '状态设置失败', '操作失败', '#cc4444');
        }
    }

    /**
     * 清除当前当日状态。
     */
    async function clearDailyStatus() {
        if (!window.chatContext?.dailyStatusUpdateUrl) {
            return;
        }

        try {
            await submitDailyStatusPayload({
                room_id: window.chatContext.roomId,
                action: 'clear',
            });

            applyCurrentUserDailyStatus(null);
            closeDailyStatusEditor();
            window.chatToast?.show({
                title: '状态已清除',
                message: '名字后方将恢复默认徽标展示。',
                icon: '♻️',
                color: '#c2410c',
                duration: 2400,
            });
        } catch (error) {
            window.chatDialog?.alert(error.message || '状态清除失败', '操作失败', '#cc4444');
        }
    }

    /**
     * 从 localStorage 读取已屏蔽的系统播报发送者列表。
     *
     * @returns {string[]}
     */
    function loadBlockedSystemSenders() {
        if (window.ChatRoomTools?.loadBlockedSystemSenders) {
            return window.ChatRoomTools.loadBlockedSystemSenders(BLOCKABLE_SYSTEM_SENDERS);
        }

        try {
            const saved = JSON.parse(localStorage.getItem(BLOCKED_SYSTEM_SENDERS_STORAGE_KEY) || '[]');
            if (!Array.isArray(saved)) {
                return [];
            }

            // 仅允许白名单内的系统播报项进入配置，避免脏数据污染。
            return saved.filter(sender => BLOCKABLE_SYSTEM_SENDERS.includes(sender));
        } catch (error) {
            return [];
        }
    }

    /**
     * 将当前屏蔽配置持久化到 localStorage。
     */
    function persistBlockedSystemSenders() {
        if (window.ChatRoomTools?.persistBlockedSystemSenders) {
            window.ChatRoomTools.persistBlockedSystemSenders(blockedSystemSenders);
            return;
        }

        localStorage.setItem(
            BLOCKED_SYSTEM_SENDERS_STORAGE_KEY,
            JSON.stringify(Array.from(blockedSystemSenders))
        );
    }

    /**
     * 判断当前禁音开关是否处于打开状态。
     *
     * @returns {boolean}
     */
    function isSoundMuted() {
        if (window.ChatRoomTools?.isSoundMuted) {
            return window.ChatRoomTools.isSoundMuted();
        }

        const muteCheckbox = document.getElementById('sound_muted');

        if (muteCheckbox) {
            return Boolean(muteCheckbox.checked);
        }

        return localStorage.getItem(CHAT_SOUND_MUTED_STORAGE_KEY) === '1';
    }

    /**
     * 获取当前聊天室偏好快照。
     *
     * @returns {Object}
     */
    function buildChatPreferencesPayload() {
        return {
            blocked_system_senders: Array.from(blockedSystemSenders),
            sound_muted: isSoundMuted(),
        };
    }

    /**
     * 将聊天室偏好写入本地缓存，供刷新前快速恢复与迁移兜底。
     */
    function persistChatPreferencesToLocal() {
        persistBlockedSystemSenders();
        if (window.ChatRoomTools?.setSoundMuted) {
            window.ChatRoomTools.setSoundMuted(isSoundMuted());
            return;
        }

        localStorage.setItem(CHAT_SOUND_MUTED_STORAGE_KEY, isSoundMuted() ? '1' : '0');
    }

    /**
     * 将当前聊天室偏好保存到当前登录账号。
     */
    async function saveChatPreferences() {
        const payload = buildChatPreferencesPayload();

        persistChatPreferencesToLocal();

        if (!window.chatContext?.chatPreferencesUrl) {
            return;
        }

        try {
            const response = await fetch(window.chatContext.chatPreferencesUrl, {
                method: 'PUT',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                },
                body: JSON.stringify(payload),
            });

            if (!response.ok) {
                throw new Error('save chat preferences failed');
            }

            const data = await response.json();
            if (data?.status === 'success') {
                window.chatContext.chatPreferences = normalizeChatPreferences(data.data || payload);
            }
        } catch (error) {
            console.error('聊天室偏好保存失败:', error);
        }
    }

    /**
     * 同步屏蔽菜单中的复选框状态。
     */
    function syncBlockedSystemSenderCheckboxes() {
        const fishingCheckbox = document.getElementById('block-sender-fishing');
        const doctorCheckbox = document.getElementById('block-sender-doctor');
        const baccaratCheckbox = document.getElementById('block-sender-baccarat');
        const horseRaceCheckbox = document.getElementById('block-sender-horse-race');
        const mysteryBoxCheckbox = document.getElementById('block-sender-mystery-box');

        if (fishingCheckbox) {
            fishingCheckbox.checked = blockedSystemSenders.has('钓鱼播报');
        }

        if (doctorCheckbox) {
            doctorCheckbox.checked = blockedSystemSenders.has('星海小博士');
        }

        if (baccaratCheckbox) {
            baccaratCheckbox.checked = blockedSystemSenders.has('百家乐');
        }

        if (horseRaceCheckbox) {
            horseRaceCheckbox.checked = blockedSystemSenders.has('跑马');
        }

        if (mysteryBoxCheckbox) {
            mysteryBoxCheckbox.checked = blockedSystemSenders.has('神秘箱子');
        }
    }

    /**
     * 根据消息内容识别其对应的屏蔽规则键。
     *
     * @param {Record<string, any>} msg 消息对象
     * @returns {string|null}
     */
    function resolveBlockedSystemSenderKey(msg) {
        const fromUser = String(msg?.from_user || '');
        const content = String(msg?.content || '');

        if (fromUser === '钓鱼播报') {
            return '钓鱼播报';
        }

        if (fromUser === '神秘箱子') {
            return '神秘箱子';
        }

        if (fromUser === '星海小博士') {
            return '星海小博士';
        }

        // 兼容旧版自动钓鱼卡购买通知：历史上该消息曾以“系统传音”发送，但正文里带有“钓鱼播报”字样。
        if ((fromUser === '系统传音' || fromUser === '系统') && (content.includes('钓鱼播报') || content.includes('自动钓鱼模式'))) {
            return '钓鱼播报';
        }

        if ((fromUser === '系统传音' || fromUser === '系统') && content.includes('神秘箱子')) {
            return '神秘箱子';
        }

        if ((fromUser === '系统传音' || fromUser === '系统') && content.includes('百家乐')) {
            return '百家乐';
        }

        if ((fromUser === '系统传音' || fromUser === '系统') && (content.includes('赛马') || content.includes('跑马'))) {
            return '跑马';
        }

        return null;
    }

    /**
     * 批量切换当前已渲染消息的显示状态。
     *
     * @param {string} blockKey 屏蔽规则键
     * @param {boolean} hidden true = 隐藏，false = 恢复显示
     */
    function setRenderedMessagesVisibilityBySender(blockKey, hidden) {
        [container, container2].forEach(targetContainer => {
            if (!targetContainer) {
                return;
            }

            targetContainer.querySelectorAll('[data-block-key]').forEach(node => {
                if (node.dataset.blockKey === blockKey) {
                    if (hidden) {
                        node.dataset.blockHidden = '1';
                        node.style.display = 'none';
                    } else if (node.dataset.blockHidden === '1') {
                        node.removeAttribute('data-block-hidden');
                        node.style.display = '';
                    }
                }
            });
        });

        if (!hidden && autoScroll) {
            container.scrollTop = container.scrollHeight;
            container2.scrollTop = container2.scrollHeight;
        }
    }

    /**
     * 切换系统播报屏蔽菜单的显示状态。
     *
     * @param {Event} event 点击事件
     */
    function toggleBlockMenu(event) {
        event.stopPropagation();
        const menu = document.getElementById('block-menu');
        const welcomeMenu = document.getElementById('welcome-menu');
        const adminMenu = document.getElementById('admin-menu');
        const featureMenu = document.getElementById('feature-menu');
        const dailyStatusEditor = document.getElementById('daily-status-editor-overlay');

        if (!menu) {
            return;
        }

        if (welcomeMenu) {
            welcomeMenu.style.display = 'none';
        }

        if (adminMenu) {
            adminMenu.style.display = 'none';
        }

        if (featureMenu) {
            featureMenu.style.display = 'none';
        }

        if (dailyStatusEditor) {
            dailyStatusEditor.style.display = 'none';
        }

        syncBlockedSystemSenderCheckboxes();
        menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
    }

    /**
     * 更新指定系统播报项的屏蔽状态，并在勾选后立即清理当前窗口。
     *
     * @param {string} sender 系统播报发送者/规则键
     * @param {boolean} blocked 是否屏蔽
     */
    function toggleBlockedSystemSender(sender, blocked) {
        if (!BLOCKABLE_SYSTEM_SENDERS.includes(sender)) {
            return;
        }

        if (blocked) {
            blockedSystemSenders.add(sender);
            // 勾选后立刻隐藏聊天室窗口内已显示的对应播报内容。
            setRenderedMessagesVisibilityBySender(sender, true);
        } else {
            blockedSystemSenders.delete(sender);
            // 取消勾选后立即恢复先前被隐藏的对应播报内容。
            setRenderedMessagesVisibilityBySender(sender, false);
        }

        persistBlockedSystemSenders();
        syncBlockedSystemSenderCheckboxes();
        void saveChatPreferences();
    }

    syncBlockedSystemSenderCheckboxes();

    /**
     * 转义会员横幅文本，避免横幅层被注入 HTML。
     */
    function escapePresenceText(text) {
        return escapeHtml(String(text ?? '')).replace(/\n/g, '<br>');
    }

    /**
     * 根据不同的会员横幅风格返回渐变与光影配置。
     * 已调优为更明亮、喜庆的配色方案。
     */
    function getVipPresenceStyleConfig(style, color) {
        const fallback = color || '#f59e0b';

        const map = {
            aurora: {
                // 鎏光星幕：金黄到明黄渐变，极具喜庆感
                gradient: `linear-gradient(135deg, #f59e0b, #fbbf24, #fef3c7)`,
                glow: `rgba(251, 191, 36, 0.4)`,
                accent: '#78350f',
            },
            storm: {
                // 雷霆风暴：明亮的湖蓝到浅蓝，清爽亮丽
                gradient: `linear-gradient(135deg, #0ea5e9, #7dd3fc, #f0f9ff)`,
                glow: `rgba(125, 211, 252, 0.4)`,
                accent: '#0369a1',
            },
            royal: {
                // 王者金辉：深金到亮金，尊贵大气
                gradient: `linear-gradient(135deg, #d97706, #fcd34d, #fffbeb)`,
                glow: `rgba(252, 211, 77, 0.4)`,
                accent: '#92400e',
            },
            cosmic: {
                // 星穹幻彩：玫红到粉色渐变，活泼吉利
                gradient: `linear-gradient(135deg, #db2777, #f472b6, #fdf2f8)`,
                glow: `rgba(244, 114, 182, 0.4)`,
                accent: '#9d174d',
            },
            farewell: {
                // 告别暮光：橙红到暖黄，温馨亮堂
                gradient: `linear-gradient(135deg, #ea580c, #fb923c, #fff7ed)`,
                glow: `rgba(251, 146, 60, 0.4)`,
                accent: '#9a3412',
            },
        };

        return map[style] || map.aurora;
    }

    /**
     * 显示会员进退场豪华横幅。
     */
    function showVipPresenceBanner(payload) {
        if (!payload || !payload.presence_text) {
            return;
        }

        const existing = document.getElementById('vip-presence-banner');
        if (existing) {
            existing.remove();
        }

        const styleConfig = getVipPresenceStyleConfig(payload.presence_banner_style, payload.presence_color);
        const bannerTypeLabel = payload.presence_type === 'leave'
            ? '离场提示'
            : (payload.presence_type === 'purchase' ? '开通喜报' : '闪耀登场');
        const banner = document.createElement('div');
        banner.id = 'vip-presence-banner';
        banner.className = 'vip-presence-banner';
        banner.innerHTML = `
            <div class="vip-presence-banner__glow" style="background:${styleConfig.glow};"></div>
            <div class="vip-presence-banner__card" style="background:${styleConfig.gradient}; border-color:${payload.presence_color || '#fff'};">
                <div class="vip-presence-banner__meta">
                    <span class="vip-presence-banner__icon">${escapeHtml(payload.presence_icon || '👑')}</span>
                    <span class="vip-presence-banner__level">${escapeHtml(payload.presence_level_name || '尊贵会员')}</span>
                    <span class="vip-presence-banner__type">${bannerTypeLabel}</span>
                </div>
                <div class="vip-presence-banner__text" style="color:${styleConfig.accent};">${escapePresenceText(payload.presence_text)}</div>
            </div>
        `;

        document.body.appendChild(banner);

        setTimeout(() => {
            banner.classList.add('is-leaving');
            setTimeout(() => banner.remove(), 700);
        }, 4200);
    }

    window.showVipPresenceBanner = showVipPresenceBanner;

    /**
     * 判断图片消息是否已经超过前端允许展示的保留期。
     */
    function isExpiredChatImageMessage(msg) {
        if (!msg) {
            return false;
        }

        if (msg.message_type === 'expired_image') {
            return true;
        }

        if (msg.message_type !== 'image') {
            return false;
        }

        if (!msg.image_url || !msg.image_thumb_url) {
            return true;
        }

        const retentionDays = parseInt(window.chatContext?.chatImageRetentionDays || 3, 10);
        const sentAtText = String(msg.sent_at || '').replace(' ', 'T');
        const sentAt = sentAtText ? new Date(sentAtText) : null;

        if (!sentAt || Number.isNaN(sentAt.getTime())) {
            return false;
        }

        return Date.now() >= sentAt.getTime() + retentionDays * 24 * 60 * 60 * 1000;
    }

    /**
     * 构建普通聊天消息的正文区域，支持缩略图与过期占位渲染。
     */
    function buildChatMessageContent(msg, fontColor) {
        const rawContent = msg.content || '';

        if (msg.message_type === 'image' && !isExpiredChatImageMessage(msg)) {
            const fullUrl = escapeHtml(msg.image_url || '');
            const thumbUrl = escapeHtml(msg.image_thumb_url || '');
            const imageName = escapeHtml(msg.image_original_name || '聊天图片');
            const captionHtml = rawContent ?
                `<span style="display:inline-block; max-width:220px; color:${fontColor}; line-height:1.55;">${rawContent}</span>` :
                '';

            return `
                <span style="display:inline-flex; align-items:flex-start; gap:6px; vertical-align:middle;">
                    <a href="${fullUrl}" data-full="${fullUrl}" data-alt="${imageName}" data-chat-image-lightbox-open
                        style="display:inline-block; border:1px solid rgba(15,23,42,.14); border-radius:10px; overflow:hidden; background:#f8fafc; box-shadow:0 2px 10px rgba(15,23,42,.10);">
                        <img src="${thumbUrl}" alt="${imageName}"
                            style="display:block; max-width:96px; max-height:96px; object-fit:cover; cursor:zoom-in;">
                    </a>
                    ${captionHtml}
                </span>
            `;
        }

        if (msg.message_type === 'expired_image' || isExpiredChatImageMessage(msg)) {
            const captionHtml = rawContent ?
                `<span style="display:inline-block; color:${fontColor}; line-height:1.55;">${rawContent}</span>` :
                '';

            return `
                <span style="display:inline-flex; align-items:center; gap:6px; vertical-align:middle;">
                    <span style="display:inline-flex; align-items:center; padding:4px 8px; border:1px dashed #94a3b8; border-radius:999px; background:#f8fafc; color:#64748b; font-size:12px;">🖼️ 图片已过期</span>
                    ${captionHtml}
                </span>
            `;
        }

        return rawContent;
    }

    // ── Tab 切换 ──────────────────────────────────────
    let _roomsRefreshTimer = null;
    let _roomsOnlineStatusCache = null;
    let _roomsOnlineStatusCacheAt = 0;
    const ROOMS_ONLINE_STATUS_CACHE_TTL = 10000;

    function switchTab(tab) {
        // 切换名单/房间 面板
        ['users', 'rooms'].forEach(t => {
            document.getElementById('panel-' + t).style.display = t === tab ? 'block' : 'none';
            document.getElementById('tab-' + t)?.classList.toggle('active', t === tab);
        });
        // 房间 Tab：立即拉取 + 每 30 秒自动刷新在线人数
        if (tab === 'rooms') {
            loadRoomsOnlineStatus();
            clearInterval(_roomsRefreshTimer);
            _roomsRefreshTimer = setInterval(() => loadRoomsOnlineStatus(true), 30000);
        } else {
            clearInterval(_roomsRefreshTimer);
            _roomsRefreshTimer = null;
        }
    }

    /**
     * 拉取所有房间在线人数并渲染到右侧面板
     */
    const _currentRoomId = {{ $room->id }};

    function loadRoomsOnlineStatus(forceRefresh = false) {
        const container = document.getElementById('rooms-online-list');
        if (!container) {
            return;
        }

        if (!forceRefresh && _roomsOnlineStatusCache && Date.now() - _roomsOnlineStatusCacheAt < ROOMS_ONLINE_STATUS_CACHE_TTL) {
            renderRoomsOnlineStatus(_roomsOnlineStatusCache, container);
            return;
        }

        fetch('{{ route('chat.rooms-online-status') }}')
            .then(r => r.json())
            .then(data => {
                _roomsOnlineStatusCache = data;
                _roomsOnlineStatusCacheAt = Date.now();
                renderRoomsOnlineStatus(data, container);
            })
            .catch(() => {
                container.innerHTML =
                    '<div style="text-align:center;color:#f00;padding:10px;font-size:11px;">加载失败</div>';
            });
    }

    /**
     * 渲染房间在线状态列表。
     *
     * @param {Object} data 接口返回数据
     * @param {HTMLElement} container 目标容器
     */
    function renderRoomsOnlineStatus(data, container) {
        if (window.ChatRoomTools?.renderRoomsOnlineStatusToContainer) {
            window.ChatRoomTools.renderRoomsOnlineStatusToContainer(data, container, {
                currentRoomId: _currentRoomId,
                variant: 'desktop',
            });
            return;
        }

        if (!data.rooms || !data.rooms.length) {
            container.innerHTML =
                '<div style="text-align:center;color:#bbb;padding:16px 0;font-size:11px;">暂无房间</div>';
            return;
        }
        const roomRows = data.rooms.map(room => {
            const roomId = Number.parseInt(room.id, 10);
            if (!Number.isInteger(roomId)) {
                return '';
            }

            const isCurrent = roomId === _currentRoomId;
            const closed = !room.door_open;
            const safeRoomName = escapeHtml(String(room.name ?? ''));
            const safeOnlineCount = Math.max(Number.parseInt(room.online, 10) || 0, 0);
            const bg = isCurrent ? '#ecf4ff' : '#fff';
            const border = isCurrent ? '#aac5f0' : '#e0eaf5';
            const nameColor = isCurrent ? '#336699' : (closed ? '#bbb' : '#444');
            const badge = safeOnlineCount > 0 ?
                `<span style="background:#e8f5e9;color:#2e7d32;border-radius:8px;padding:0 5px;font-size:10px;font-weight:bold;white-space:nowrap;flex-shrink:0;">${safeOnlineCount} 人</span>` :
                `<span style="background:#f5f5f5;color:#bbb;border-radius:8px;padding:0 5px;font-size:10px;white-space:nowrap;flex-shrink:0;">空</span>`;
            const currentTag = isCurrent ?
                `<span style="font-size:9px;color:#336699;opacity:.7;margin-left:3px;">当前</span>` :
                '';
            const clickHandler = isCurrent ? '' : `onclick="location.href='/room/${roomId}'"`;

            return `<div ${clickHandler}
                        style="display:flex;align-items:center;justify-content:space-between;
                               padding:5px 8px;margin:2px 3px;border-radius:5px;
                               border:1px solid ${border};background:${bg};
                               cursor:${isCurrent ? 'default' : 'pointer'};
                               transition:background .15s;"
                        onmouseover="if(${!isCurrent}) this.style.background='#ddeeff';"
                        onmouseout="this.style.background='${bg}';">
                        <span style="color:${nameColor};font-size:11px;font-weight:${isCurrent?'bold':'normal'};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;margin-right:4px;">
                            ${safeRoomName}${currentTag}
                        </span>
                        ${badge}
                    </div>`;
        }).filter(Boolean).join('');

        container.innerHTML = roomRows ||
            '<div style="text-align:center;color:#bbb;padding:16px 0;font-size:11px;">暂无房间</div>';
    }




    // ── 欢迎语快捷菜单 ──────────────────────────────────────
    /**
     * 切换欢迎语下拉浮层的显示/隐藏
     */
    function toggleWelcomeMenu(event) {
        event.stopPropagation();
        const menu = document.getElementById('welcome-menu');
        const adminMenu = document.getElementById('admin-menu');
        const blockMenu = document.getElementById('block-menu');
        const featureMenu = document.getElementById('feature-menu');
        const dailyStatusEditor = document.getElementById('daily-status-editor-overlay');
        if (!menu) {
            return;
        }
        if (adminMenu) {
            adminMenu.style.display = 'none';
        }
        if (blockMenu) {
            blockMenu.style.display = 'none';
        }
        if (featureMenu) {
            featureMenu.style.display = 'none';
        }
        if (dailyStatusEditor) {
            dailyStatusEditor.style.display = 'none';
        }
        menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
    }

    /**
     * 切换顶部管理菜单的显示状态。
     */
    function toggleAdminMenu(event) {
        event.stopPropagation();
        const menu = document.getElementById('admin-menu');
        const welcomeMenu = document.getElementById('welcome-menu');
        const blockMenu = document.getElementById('block-menu');
        const featureMenu = document.getElementById('feature-menu');
        const dailyStatusEditor = document.getElementById('daily-status-editor-overlay');
        if (!menu) {
            return;
        }
        if (welcomeMenu) {
            welcomeMenu.style.display = 'none';
        }
        if (blockMenu) {
            blockMenu.style.display = 'none';
        }
        if (featureMenu) {
            featureMenu.style.display = 'none';
        }
        if (dailyStatusEditor) {
            dailyStatusEditor.style.display = 'none';
        }
        menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
    }

    /**
     * 执行管理菜单中的快捷操作，并在执行前关闭菜单。
     *
     * @param {string} action 管理动作类型
     */
    function runAdminAction(action) {
        const menu = document.getElementById('admin-menu');
        if (menu) {
            menu.style.display = 'none';
        }

        switch (action) {
            case 'announcement':
                promptAnnouncement();
                break;
            case 'announce-message':
                promptAnnounceMessage();
                break;
            case 'admin-clear':
                adminClearScreen();
                break;
            case 'red-packet':
                sendRedPacket();
                break;
            case 'loss-cover':
                openAdminBaccaratLossCoverModal();
                break;
            case 'refresh-all':
                refreshAllBrowsers();
                break;
            default:
                break;
        }
    }

    /**
     * 选择特效后关闭菜单，并沿用原有管理员特效触发逻辑。
     *
     * @param {string} type 特效类型
     */
    function selectEffect(type) {
        const menu = document.getElementById('admin-menu');
        if (menu) {
            menu.style.display = 'none';
        }

        triggerEffect(type);
    }

    /**
     * 站长通知当前房间所有在线用户刷新页面。
     */
    async function refreshAllBrowsers() {
        if (!window.chatContext?.isSiteOwner || !window.chatContext?.refreshAllUrl) {
            window.chatDialog?.alert('仅站长可执行全员刷新。', '无权限', '#cc4444');
            return;
        }

        const confirmed = await window.chatDialog?.confirm(
            '确定通知当前房间所有在线用户刷新页面吗？\n适用于功能更新后强制同步最新按钮与权限状态。',
            '♻️ 刷新全员',
            '#0f766e'
        );

        if (!confirmed) {
            return;
        }

        try {
            const response = await fetch(window.chatContext.refreshAllUrl, {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                },
                body: JSON.stringify({
                    room_id: window.chatContext.roomId,
                    reason: '功能更新，站长要求刷新页面',
                }),
            });
            const data = await response.json();

            if (data.status === 'success') {
                window.chatToast?.show({
                    title: '已发送刷新通知',
                    message: data.message,
                    icon: '♻️',
                    color: '#0f766e',
                    duration: 3500,
                });
                return;
            }

            window.chatDialog?.alert(data.message || '发送刷新通知失败', '操作失败', '#cc4444');
        } catch (error) {
            window.chatDialog?.alert('网络异常，请稍后再试', '错误', '#cc4444');
        }
    }

    /**
     * 将选中的欢迎语模板填入输入框，{name} 替换为当前选中的聊天对象，
     * 并在前面加上「部门 职务 姓名：」前缀，然后自动发送
     *
     * @param {string} tpl 欢迎语模板，含 {name} 占位符
     */
    function sendWelcomeTpl(tpl) {
        const toUser = document.getElementById('to_user')?.value || '大家';
        const name = toUser === '大家' ? '大家' : toUser;
        const prefix = window.chatContext?.welcomePrefix || window.chatContext?.username || '';
        const body = tpl.replace(/\{name\}/g, name);
        // 拼接格式：「部门 职务 姓名：欢迎语」
        const msg = `${prefix}：${body}`;

        const input = document.getElementById('content');
        if (input) {
            input.value = msg;
        }

        const menu = document.getElementById('welcome-menu');
        if (menu) {
            menu.style.display = 'none';
        }

        // 临时把 action 设为「欢迎」，让消息在聊天窗口以瓦蓝边框样式显示
        const actionSel = document.getElementById('action');
        const prevAction = actionSel?.value || '';
        if (actionSel) {
            actionSel.value = '欢迎';
        }

        // 自动触发发送
        sendMessage(null).finally(() => {
            // 发完后恢复原来的 action
            if (actionSel) {
                actionSel.value = prevAction;
            }
        });
    }

    // 点击页面任意位置，关闭欢迎语浮层
    document.addEventListener('click', function() {
        const menu = document.getElementById('welcome-menu');
        if (menu) {
            menu.style.display = 'none';
        }

        const adminMenu = document.getElementById('admin-menu');
        if (adminMenu) {
            adminMenu.style.display = 'none';
        }

        const blockMenu = document.getElementById('block-menu');
        if (blockMenu) {
            blockMenu.style.display = 'none';
        }

        const featureMenu = document.getElementById('feature-menu');
        if (featureMenu) {
            featureMenu.style.display = 'none';
        }

        const dailyStatusEditor = document.getElementById('daily-status-editor-overlay');
        if (dailyStatusEditor) {
            dailyStatusEditor.style.display = 'none';
        }
    });

    // ── 动作选择 ──────────────────────────────────────
    /**
     * 设置发言动作并聚焦输入框
     *
     * @param {string} act 动作名称
     */
    function setAction(act) {
        document.getElementById('action').value = act;
        switchTab('users');
        document.getElementById('content').focus();
    }

    // ── 自动滚屏 ──────────────────────────────────────
    const autoScrollEl = document.getElementById('auto_scroll');
    if (autoScrollEl) {
        autoScrollEl.addEventListener('change', function() {
            autoScroll = this.checked;
        });
    }

    // ── 滚动到底部 ───────────────────────────────────
    /**
     * 将公聊窗口滚动到最新消息（受 autoScroll 开关控制）
     */
    function scrollToBottom() {
        if (autoScroll) {
            container.scrollTop = container.scrollHeight;
        }
    }

    /**
     * 把小气泡提示定位到图标旁边，不侵入右侧名单结构。
     *
     * @param {HTMLElement} trigger 当前悬浮的图标元素
     */
    function positionHoverTooltip(trigger) {
        if (!hoverTooltip || !trigger) {
            return;
        }

        const offset = 10;
        const rect = trigger.getBoundingClientRect();
        const tooltipWidth = hoverTooltip.offsetWidth;
        const tooltipHeight = hoverTooltip.offsetHeight;
        const fitsRight = rect.right + offset + tooltipWidth <= window.innerWidth - 8;
        const side = fitsRight ? 'right' : 'left';
        const nextLeft = fitsRight
            ? rect.right + offset
            : Math.max(8, rect.left - tooltipWidth - offset);
        const nextTop = Math.min(
            Math.max(8, rect.top + (rect.height - tooltipHeight) / 2),
            window.innerHeight - tooltipHeight - 8
        );

        hoverTooltip.dataset.side = side;
        hoverTooltip.style.left = `${nextLeft}px`;
        hoverTooltip.style.top = `${nextTop}px`;
    }

    /**
     * 显示图标旁边的小气泡文字提示。
     *
     * @param {HTMLElement|null} trigger 当前悬浮的图标元素
     */
    function showHoverTooltip(trigger) {
        if (!hoverTooltip || !trigger) {
            return;
        }

        const tooltipText = trigger.dataset.instantTooltip || '';
        if (!tooltipText) {
            return;
        }

        activeTooltipTrigger = trigger;
        hoverTooltip.textContent = tooltipText;
        hoverTooltip.style.display = 'block';
        positionHoverTooltip(trigger);
    }

    /**
     * 隐藏图标提示气泡。
     */
    function hideHoverTooltip() {
        if (!hoverTooltip) {
            return;
        }

        activeTooltipTrigger = null;
        hoverTooltip.style.display = 'none';
        hoverTooltip.textContent = '';
        delete hoverTooltip.dataset.side;
    }

    // 通过事件委托处理动态生成的徽章提示，确保 hover 后立刻显示。
    document.addEventListener('mouseover', (event) => {
        const trigger = event.target.closest('.user-badge-icon[data-instant-tooltip]');
        if (!trigger) {
            return;
        }

        showHoverTooltip(trigger);
    });

    document.addEventListener('mouseout', (event) => {
        const trigger = event.target.closest('.user-badge-icon[data-instant-tooltip]');
        if (!trigger || trigger !== activeTooltipTrigger) {
            return;
        }

        if (event.relatedTarget && trigger.contains(event.relatedTarget)) {
            return;
        }

        hideHoverTooltip();
    });

    window.addEventListener('scroll', () => {
        if (activeTooltipTrigger) {
            positionHoverTooltip(activeTooltipTrigger);
        }
    }, true);

    window.addEventListener('resize', () => {
        if (activeTooltipTrigger) {
            positionHoverTooltip(activeTooltipTrigger);
        }
    });

    // ── 渲染在线人员列表（支持排序） ──────────────────
    /**
     * 核心渲染函数：将在线用户渲染到指定容器（桌面端名单区和手机端抽屉共用）
     *
     * @param {HTMLElement} targetContainer 目标 DOM 容器
     * @param {string}      sortBy          排序方式：'default' | 'name' | 'level'
     * @param {string}      keyword         搜索关键词（小写）
     */
    function _renderUserListToContainer(targetContainer, sortBy, keyword) {
        if (!targetContainer) return;
        const fragment = document.createDocumentFragment();

        // 在列表顶部添加"大家"条目（原版风格）
        let allDiv = document.createElement('div');
        allDiv.className = 'user-item';
        allDiv.dataset.userListEveryone = '1';
        allDiv.innerHTML = '<span class="user-name" style="padding-left: 4px; color: navy;">大家</span>';
        fragment.appendChild(allDiv);

        // ── AI 小助手（已在 onlineUsers 中动态维护，移除硬编码）──

        // 构建用户数组并排序
        let userArr = [];
        for (let username in onlineUsers) {
            userArr.push({
                username,
                ...onlineUsers[username]
            });
        }

        if (sortBy === 'name') {
            userArr.sort((a, b) => a.username.localeCompare(b.username, 'zh'));
        } else if (sortBy === 'level') {
            userArr.sort((a, b) => (b.user_level || 0) - (a.user_level || 0));
        }

        userArr.forEach(user => {
            const username = user.username;

            // 搜索过滤
            if (keyword && !username.toLowerCase().includes(keyword)) return;

            let item = document.createElement('div');
            item.className = 'user-item';
            item.dataset.username = username;
            item.dataset.userListEntry = '1';

            const headface = (user.headface || '1.gif');
            const headImgSrc = headface.startsWith('storage/') ? '/' + headface : '/images/headface/' +
                headface;

            const badges = buildUserBadgeHtml(user, username);

            // 女生名字使用玫粉色
            const nameColor = (user.sex == 2) ? 'color:#e91e8c;' : '';
            item.innerHTML = `
                <img class="user-head" src="${headImgSrc}" onerror="this.src='/images/headface/1.gif'">
                <span class="user-name" style="${nameColor}">${username}</span>
                <span class="user-badge-slot">${badges}</span>
            `;

            // 具体点击、双击与手机双触发由 Vite 的 right-panel.js 统一事件委托处理。
            fragment.appendChild(item);
        });

        targetContainer.replaceChildren(fragment);
        refreshRenderedUserBadges(targetContainer);
    }

    function renderUserList() {
        if (userListRenderTimer) {
            window.clearTimeout(userListRenderTimer);
            userListRenderTimer = null;
        }

        const selectFragment = document.createDocumentFragment();
        const everyoneOption = document.createElement('option');
        everyoneOption.value = '大家';
        everyoneOption.textContent = '大家';
        selectFragment.appendChild(everyoneOption);

        // 获取排序方式和搜索词
        const sortSelect = document.getElementById('user-sort-select');
        const sortBy = sortSelect ? sortSelect.value : 'default';
        const searchInput = document.getElementById('user-search-input');
        const keyword = searchInput ? searchInput.value.trim().toLowerCase() : '';

        // 调用核心渲染（桌面端名单容器）
        _renderUserListToContainer(userList, sortBy, keyword);

        // 下拉框里如果 AI在场，可以直接选
        for (let username in onlineUsers) {
            if (username !== window.chatContext.username) {
                let option = document.createElement('option');
                option.value = username;
                let text = username;
                if (username === 'AI小班长') {
                    text = '🤖 AI小班长';
                }
                option.textContent = text;
                selectFragment.appendChild(option);
            }
        }
        toUserSelect.replaceChildren(selectFragment);

        const count = Object.keys(onlineUsers).length;
        onlineCount.innerText = count;
        onlineCountBottom.innerText = count;
        const footer = document.getElementById('online-count-footer');
        if (footer) {
            footer.innerText = count;
        }

        // 派发用户列表更新事件，供手机端抽屉同步
        window.dispatchEvent(new Event('chatroom:users-updated'));
    }

    /**
     * 合并高频在线名单变动，避免 Presence 连续进出时重复重建名单 DOM。
     *
     * @param {number} delay 等待毫秒数
     */
    function scheduleRenderUserList(delay = 120) {
        if (userListRenderTimer) {
            window.clearTimeout(userListRenderTimer);
        }

        userListRenderTimer = window.setTimeout(() => {
            userListRenderTimer = null;
            renderUserList();
        }, delay);
    }

    /**
     * 获取用户当前仍然有效的当日状态。
     *
     * @param {Record<string, any>} user 用户在线载荷
     * @returns {Object|null}
     */
    function resolveUserDailyStatus(user) {
        return normalizeDailyStatus(user);
    }

    /**
     * 构建职务 / 管理员徽标。
     *
     * @param {Record<string, any>} user 用户在线载荷
     * @param {string} username 用户名
     * @returns {string}
     */
    function buildUserPrimaryBadgeHtml(user, username) {
        if (user.position_icon) {
            const posTitle = (user.position_name || '在职') + ' · ' + username;
            const safePosTitle = escapeHtml(String(posTitle));
            const safePositionIcon = escapeHtml(String(user.position_icon || '🎖️'));

            return `<span class="user-badge-icon" style="font-size:13px; margin-left:2px;" data-instant-tooltip="${safePosTitle}">${safePositionIcon}</span>`;
        }

        if (user.is_admin) {
            return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px;" data-instant-tooltip="最高统帅">🎖️</span>`;
        }

        return '';
    }

    /**
     * 构建用户 VIP 徽标。
     *
     * @param {Record<string, any>} user 用户在线载荷
     * @returns {string}
     */
    function buildUserVipBadgeHtml(user) {
        if (!user.vip_icon) {
            return '';
        }

        const vipColor = user.vip_color || '#f59e0b';
        const safeVipTitle = escapeHtml(String(user.vip_name || 'VIP'));
        const safeVipIcon = escapeHtml(String(user.vip_icon || '👑'));

        return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px; color:${vipColor};" data-instant-tooltip="${safeVipTitle}">${safeVipIcon}</span>`;
    }

    /**
     * 构建状态徽标。
     *
     * @param {Record<string, any>} user 用户在线载荷
     * @returns {string}
     */
    function buildUserStatusBadgeHtml(user) {
        const status = resolveUserDailyStatus(user);
        if (!status) {
            return '';
        }

        const safeIcon = escapeHtml(status.icon);
        const safeLabel = escapeHtml(status.label);
        const safeTooltip = escapeHtml(`${status.group} · ${status.label}`);

        return `
            <span class="user-badge-icon" style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;max-width:78px;overflow:hidden;white-space:nowrap;border-radius:999px;background:#eef2ff;border:1px solid #c7d2fe;color:#4338ca;font-size:11px;line-height:18px;vertical-align:middle;"
                data-instant-tooltip="${safeTooltip}">
                <span style="font-size:11px;line-height:1;flex-shrink:0;">${safeIcon}</span>
                <span style="line-height:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${safeLabel}</span>
            </span>
        `;
    }

    /**
     * 构建签到身份徽标。
     *
     * @param {Record<string, any>} user 用户在线载荷
     * @returns {string}
     */
    function buildUserSignIdentityBadgeHtml(user) {
        const identityKey = String(user.sign_identity_key ?? user.sign_identity ?? '');
        const identityLabel = String(user.sign_identity_label ?? user.sign_identity_name ?? '');
        const identityIcon = String(user.sign_identity_icon ?? '');

        if (!identityKey || !identityLabel || !identityIcon) {
            return '';
        }

        const color = String(user.sign_identity_color || '#0f766e');
        const bgColor = String(user.sign_identity_bg_color || '#ccfbf1');
        const borderColor = String(user.sign_identity_border_color || '#5eead4');
        const safeIcon = escapeHtml(identityIcon);
        const safeLabel = escapeHtml(identityLabel);
        const safeTooltip = escapeHtml(`签到 · ${identityLabel}`);

        return `
            <span class="user-badge-icon" style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;max-width:78px;overflow:hidden;white-space:nowrap;border-radius:999px;background:${bgColor};border:1px solid ${borderColor};color:${color};font-size:11px;line-height:18px;vertical-align:middle;"
                data-instant-tooltip="${safeTooltip}">
                <span style="font-size:11px;line-height:1;flex-shrink:0;">${safeIcon}</span>
                <span style="line-height:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${safeLabel}</span>
            </span>
        `;
    }

    /**
     * 按 3 秒节奏在签到身份、状态、职务/管理、VIP 徽标之间轮换。
     *
     * @param {Record<string, any>} user 用户在线载荷
     * @param {string} username 用户名
     * @returns {string}
     */
    function buildUserBadgeHtml(user, username) {
        const badges = [
            buildUserSignIdentityBadgeHtml(user),
            buildUserStatusBadgeHtml(user),
            buildUserPrimaryBadgeHtml(user, username),
            buildUserVipBadgeHtml(user),
        ].filter(Boolean);

        if (badges.length === 0) {
            return '';
        }

        return badges[userBadgeRotationTick % badges.length];
    }

    /**
     * 仅刷新当前已渲染用户行的徽标槽位，避免重建头像节点造成闪烁。
     *
     * @param {HTMLElement|Document} scope 需要刷新的 DOM 范围
     */
    function refreshRenderedUserBadges(scope = document) {
        scope.querySelectorAll('.user-item[data-username]').forEach((item) => {
            const username = item.dataset.username;
            const badgeSlot = item.querySelector('.user-badge-slot');

            if (!username || !badgeSlot) {
                return;
            }

            badgeSlot.innerHTML = buildUserBadgeHtml(onlineUsers[username] || {}, username);
        });
    }

    // 名单中多种徽标轮换展示时，每 3 秒只刷新徽标槽位，不重建头像行。
    window.setInterval(() => {
        if (document.hidden) {
            return;
        }

        userBadgeRotationTick = (userBadgeRotationTick + 1) % 4;
        refreshRenderedUserBadges(userList);
        const mobileUsersList = document.getElementById('mob-online-users-list');
        if (mobileUsersList?.offsetParent !== null) {
            refreshRenderedUserBadges(mobileUsersList);
        }
        syncDailyStatusUi();
    }, 3000);

    /**
     * 调度用户列表搜索过滤，避免每个按键都同步扫描名单 DOM。
     */
    function scheduleFilterUserList() {
        if (userFilterRenderTimer !== null) {
            return;
        }

        const scheduleFilter = window.requestAnimationFrame || ((callback) => window.setTimeout(callback, 16));
        userFilterRenderTimer = scheduleFilter(() => {
            userFilterRenderTimer = null;
            filterUserList();
        });
    }

    /**
     * 搜索/过滤用户列表
     */
    function filterUserList() {
        const searchInput = document.getElementById('user-search-input');
        const keyword = searchInput ? searchInput.value.trim().toLowerCase() : '';
        const items = userList.querySelectorAll('.user-item');

        items.forEach(item => {
            if (!keyword) {
                item.style.display = '';
                return;
            }
            const name = (item.dataset.username || item.textContent || '').toLowerCase();
            item.style.display = name.includes(keyword) ? '' : 'none';
        });
    }






    /**
     * 追加消息到聊天窗格（原版风格：非气泡模式，逐行显示）
     */
    function appendMessage(msg, renderBatch = null) {
        // 记录拉取到的最大消息ID，用于本地清屏功能
        if (msg && msg.id > _maxMsgId) {
            _maxMsgId = msg.id;
        }

        const isMe = msg.from_user === window.chatContext.username;
        const fontColor = msg.font_color || '#000000';
        const blockRuleKey = resolveBlockedSystemSenderKey(msg);
        const shouldHideByBlock = blockRuleKey ? blockedSystemSenders.has(blockRuleKey) : false;

        const div = document.createElement('div');
        div.className = 'msg-line';
        if (msg?.from_user) {
            div.dataset.fromUser = msg.from_user;
        }
        if (blockRuleKey) {
            div.dataset.blockKey = blockRuleKey;
        }

        const timeStr = msg.sent_at || '';
        let timeStrOverride = false;

        // 系统用户名列表（不可被选为聊天对象）
        const systemUsers = ['钓鱼播报', '星海小博士', '系统传音', '系统公告', '送花播报', '系统', '欢迎', '系统播报', '神秘箱子'];

        // 动作文字映射表：情绪型（着/地，放"对"之前）和动作型（了，替换"对X说"）
        const actionTextMap = {
            '微笑': {
                type: 'emotion',
                word: '微笑着'
            },
            '大笑': {
                type: 'emotion',
                word: '大笑着'
            },
            '愤怒': {
                type: 'emotion',
                word: '愤怒地'
            },
            '哭泣': {
                type: 'emotion',
                word: '哭泣着'
            },
            '害羞': {
                type: 'emotion',
                word: '害羞地'
            },
            '鄙视': {
                type: 'emotion',
                word: '鄙视地'
            },
            '得意': {
                type: 'emotion',
                word: '得意地'
            },
            '疑惑': {
                type: 'emotion',
                word: '疑惑地'
            },
            '同情': {
                type: 'emotion',
                word: '同情地'
            },
            '无奈': {
                type: 'emotion',
                word: '无奈地'
            },
            '拳打': {
                type: 'verb',
                word: '拳打了'
            },
            '飞吻': {
                type: 'verb',
                word: '飞吻了'
            },
            '偷看': {
                type: 'verb',
                word: '偷看了'
            },
        };
        // 生成自然语序的动作串：情绪型=[人][着/地]对[目标][verb]：；动作型=[人][了][目标]，[verb]：
        const buildActionStr = (action, fromHtml, toHtml, verb = '说') => {
            const info = actionTextMap[action];
            if (!info) return `${fromHtml}对${toHtml}${escapeHtml(String(action || ''))}${verb}：`;
            if (info.type === 'emotion') return `${fromHtml}${info.word}对${toHtml}${verb}：`;
            return `${fromHtml}${info.word}${toHtml}，${verb}：`;
        };
        // 判断 【】 内的内容是否是游戏/活动标签而非真实用户名
        // 规则：命中已知游戏前缀，或内容含空格（如「双色球 第012期 开奖」）
        const isGameLabel = (name) => {
            const gamePrefixes = ['五子棋', '双色球', '钓鱼', '老虎机', '百家乐', '赛马'];
            if (gamePrefixes.some(p => name.startsWith(p))) return true;
            // 含空格 → 一定不是用户名（用户名不允许含空格）
            if (name.includes(' ')) return true;
            return false;
        };

        // 用户名（单击切换发言对象，双击查看资料；系统用户或游戏标签仅显示文本）
        const clickableUser = (uName, color) => {
            if (uName === 'AI小班长') {
                return `<span class="msg-user" data-u="${uName}" style="color: ${color}; cursor: pointer;" onclick="switchTarget('${uName}')" ondblclick="openUserCard('${uName}')">${uName}</span>`;
            }
            if (systemUsers.includes(uName) || isGameLabel(uName)) {
                return `<span class="msg-user" style="color: ${color};">${uName}</span>`;
            }
            return `<span class="msg-user" data-u="${uName}" style="color: ${color}; cursor: pointer;" onclick="switchTarget('${uName}')" ondblclick="openUserCard('${uName}')">${uName}</span>`;
        };

        // 普通用户（包括 AI小班长）用数据库头像，播报类用特殊喇叭图标
        const senderInfo = onlineUsers[msg.from_user];
        const senderHead = ((senderInfo && senderInfo.headface) || '1.gif');
        let headImgSrc = senderHead.startsWith('storage/') ? '/' + senderHead : `/images/headface/${senderHead}`;
        if (msg.from_user.endsWith('播报') || msg.from_user === '星海小博士' || msg.from_user === '系统传音' || msg.from_user === '系统公告') {
            headImgSrc = '/images/bugle.png';
        }
        const headImg =
            `<img src="${headImgSrc}" style="display:inline;width:16px;height:16px;vertical-align:middle;margin-right:2px;mix-blend-mode: multiply;" onerror="this.src='/images/headface/1.gif'">`;
        const messageBodyHtml = buildChatMessageContent(msg, fontColor);

        let html = '';

        // 第一个判断分支：如果是纯 HTML 系统的进出播报
        if (msg.action === 'system_welcome') {
            div.style.cssText = 'margin: 3px 0;';
            const iconImg =
                `<img src="/images/bugle.png" style="display:inline;width:16px;height:16px;vertical-align:middle;margin-right:2px;mix-blend-mode: multiply;" onerror="this.src='/images/headface/1.gif'">`;

            let parsedContent = msg.content;
            parsedContent = parsedContent.replace(/【([^】]+)】/g, function(match, uName) {
                return '【' + clickableUser(uName, '#000099') + '】';
            });

            html = `${iconImg} ${parsedContent}`;
        }
        // 会员专属进退场播报：更明亮喜庆的卡片化样式，由外层额外触发豪华横幅。
        else if (msg.action === 'vip_presence') {
            const accent = msg.presence_color || '#f59e0b';
            // 优化背景为明亮的白色渐变，带上会员色调的淡影，显得吉利大气
            div.style.cssText =
                `background: linear-gradient(135deg, #ffffff, ${accent}08); border: 2px solid ${accent}44; border-radius: 16px; padding: 12px 16px; margin: 8px 0; box-shadow: 0 4px 15px ${accent}15; position: relative; overflow: hidden;`;
            
            const icon = escapeHtml(msg.presence_icon || '👑');
            const levelName = escapeHtml(msg.presence_level_name || '尊贵会员');
            const typeLabel = msg.presence_type === 'leave'
                ? '华丽离场'
                : (msg.presence_type === 'purchase' ? '荣耀开通' : '荣耀入场');
            const safeText = escapePresenceText(msg.presence_text || '');

            html = `
                <div style="display:flex;align-items:center;gap:12px;">
                    <div style="width:48px;height:48px;border-radius:14px;background:linear-gradient(135deg, ${accent}, #fbbf24);display:flex;align-items:center;justify-content:center;font-size:24px;box-shadow: 0 4px 12px ${accent}44; flex-shrink: 0;">${icon}</div>
                    <div style="min-width:0;flex:1;">
                        <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
                            <span style="font-size:13px;font-weight:900;letter-spacing:.05em;color:${accent}; text-shadow: 0.5px 0.5px 0px rgba(0,0,0,0.05);">${typeLabel}</span>
                            <span style="font-size:13px;color:#475569;font-weight:bold;">${levelName}</span>
                            <span style="font-size:11px;color:#94a3b8;">(${timeStr})</span>
                        </div>
                        <div style="margin-top:4px;font-size:15px;line-height:1.6;color:#1e293b;font-weight:500;">${safeText}</div>
                    </div>
                    <div style="position:absolute; right:-10px; bottom:-10px; font-size:60px; opacity:0.05; transform:rotate(-15deg); pointer-events:none;">${icon}</div>
                </div>
            `;
            timeStrOverride = true;
        }
        // 贾妖语 —— 蓝色左边框渐变样式，比 系统公告 低调
        else if (msg.action === '欢迎') {
            div.style.cssText =
                'background: linear-gradient(135deg, #eff6ff, #f0f9ff); border: 1.5px solid #3b82f6; border-radius: 5px; padding: 5px 10px; margin: 3px 0; box-shadow: 0 1px 3px rgba(59,130,246,0.12);';

            let parsedContent = msg.content;
            parsedContent = parsedContent.replace(/【([^】]+)】/g, function(match, uName) {
                return '【' + clickableUser(uName, '#1d4ed8') + '】';
            });

            html =
                `<div style="color: #1e40af;">&#x1F4AC; ${parsedContent} <span style="color: #93c5fd; font-size: 11px; font-weight: normal;">(${timeStr})</span></div>`;
            timeStrOverride = true;
        }
        // 接下来再判断各类发话人
        else if (systemUsers.includes(msg.from_user)) {
            if (msg.from_user === '系统公告') {
                // 管理员公告：大字醒目红框样式
                div.style.cssText =
                    'background: linear-gradient(135deg, #fef2f2, #fff1f2); border: 2px solid #ef4444; border-radius: 8px; padding: 10px 14px; margin: 6px 0; box-shadow: 0 3px 8px rgba(239,68,68,0.16);';

                let parsedContent = msg.content;
                parsedContent = parsedContent.replace(/【([^】]+)】/g, function(match, uName) {
                    return '【' + clickableUser(uName, '#dc2626') + '】';
                });

                html =
                    `<div style="font-size: 18px; line-height: 1.75; font-weight: 800; color: #dc2626;">${parsedContent} <span style="color: #999; font-size: 14px; font-weight: 500;">(${timeStr})</span></div>`;
                timeStrOverride = true;
            } else if (msg.from_user === '系统传音') {
                // 自动升级播报 / 赠礼通知 / 彩票购买广播：金色左边框，轻量提示样式，不喧宾夺主
                // 解析内容中 【用户名】 片段，使其支持单击（切换发言对象）和双击（查看名片）
                div.style.cssText =
                    'background: #fffbeb; border-left: 3px solid #d97706; border-radius: 4px; padding: 4px 10px; margin: 2px 0;';
                let sysTranContent = msg.content;
                sysTranContent = sysTranContent.replace(/【([^】]+)】/g, function(match, uName) {
                    return '【' + clickableUser(uName, '#000099') + '】';
                });
                html =
                    `<span style="color: #b45309;">🌟 ${sysTranContent}</span>`;
            } else if (msg.from_user === '系统' && msg.to_user && msg.to_user !== '大家') {
                // 系统私人通知（自动存点等）：无头像，绿色左边框简洁条形样式
                div.style.cssText =
                    'background:#f0fdf4;border-left:3px solid #16a34a;border-radius:4px;padding:3px 8px;margin:2px 0;';
                html =
                    `<span style="color:#16a34a;font-weight:bold;">📢 系统：</span><span style="color:#15803d;">${msg.content}</span>`;
            } else {
                // 其他系统用户（钓鱼播报、送花播报、AI小班长等）：普通样式
                let giftHtml = '';
                if (msg.gift_image) {
                    giftHtml =
                        `<img src="${msg.gift_image}" alt="${msg.gift_name || ''}" style="display:inline-block;width:40px;height:40px;vertical-align:middle;margin-left:6px;animation:giftBounce 0.6s ease-in-out;">`;
                }

                // 让带有【用户名】的系统通知变成可点击和双击的蓝色用户标
                let parsedContent = msg.content;
                // 利用正则匹配【用户名】结构，捕获组 $1 即是里面真正的用户名
                parsedContent = parsedContent.replace(/【([^】]+)】/g, function(match, uName) {
                    return '【' + clickableUser(uName, '#000099') + '】';
                });

                html =
                    `${headImg}<span style="font-weight: bold;">${clickableUser(msg.from_user, fontColor)}：</span><span class="msg-content" style="color: ${fontColor}">${parsedContent}</span>${giftHtml}`;
            }
        } else if (msg.is_secret) {
            if (msg.from_user === '系统') {
                // 系统悄悄通知（自动存点等）：绿色左边框条形，无头像，不显示"悄悄说"
                div.style.cssText =
                    'background:#f0fdf4;border-left:3px solid #16a34a;border-radius:4px;padding:3px 8px;margin:2px 0;font-size:12px;';
                html =
                    `<span style="color:#16a34a;font-weight:bold;">📢 系统：</span><span style="color:#15803d;">${msg.content}</span>`;
            } else {
                // 普通悄悄话样式（原版：紫色斜体，使用自然语序动作）
                const fromHtml = clickableUser(msg.from_user, '#cc00cc');
                const toHtml = clickableUser(msg.to_user, '#cc00cc');
                const verbStr = msg.action ?
                    buildActionStr(msg.action, fromHtml, toHtml, '悄悄说') :
                    `${fromHtml}对${toHtml}悄悄说：`;
                html =
                    `${headImg}<span class="msg-secret">${verbStr}</span><span class="msg-content" style="color: ${fontColor}; font-style: italic;">${messageBodyHtml}</span>`;
            }
        } else if (msg.to_user && msg.to_user !== '大家') {
            // 对特定对象说话
            const fromHtml = clickableUser(msg.from_user, '#000099');
            const toHtml = clickableUser(msg.to_user, '#000099');
            const verbStr = msg.action ?
                buildActionStr(msg.action, fromHtml, toHtml) :
                `${fromHtml}对${toHtml}说：`;
            html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${messageBodyHtml}</span>`;
        } else {
            // 对大家说话
            const fromHtml = clickableUser(msg.from_user, '#000099');
            const verbStr = msg.action ?
                buildActionStr(msg.action, fromHtml, '大家') :
                `${fromHtml}对大家说：`;
            html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${messageBodyHtml}</span>`;
        }

        if (!timeStrOverride) {
            html += ` <span class="msg-time">(${timeStr})</span>`;
        }
        div.innerHTML = html;

        // 命中屏蔽规则时，消息仍保留在 DOM 中，便于取消屏蔽后立即恢复显示。
        if (shouldHideByBlock) {
            div.dataset.blockHidden = '1';
            div.style.display = 'none';
        }

        // 后端下发的带有 welcome_user 的也是系统欢迎/离开消息，加上属性标记
        if (msg.welcome_user) {
            div.setAttribute('data-system-user', msg.welcome_user);
            // 收到后端来的新欢迎消息时，把界面上该用户旧的都删掉
            const welcomeSelector = `[data-system-user="${msg.welcome_user}"]`;
            const oldWelcomes = container.querySelectorAll(welcomeSelector);
            oldWelcomes.forEach(el => el.remove());
            renderBatch?.publicFragment.querySelectorAll(welcomeSelector).forEach(el => el.remove());
            renderBatch?.privateFragment.querySelectorAll(welcomeSelector).forEach(el => el.remove());
        }

        // 路由规则（复刻原版）：
        // 公众窗口(say1)：别人的公聊消息
        // 包厢窗口(say2)：自己发的消息 + 悄悄话 + 对自己说的消息
        const isRelatedToMe = isMe ||
            msg.is_secret ||
            msg.to_user === window.chatContext.username;

        // 存点通知：标记 data-autosave 属性，每次渲染时先删除旧的，实现"滚动替换"效果
        const isAutoSave = (msg.from_user === '系统' || msg.from_user === '') &&
            msg.content && (msg.content.includes('自动存点') || msg.content.includes('手动存点'));
        if (isAutoSave) {
            div.dataset.autosave = '1';
        }

        if (isRelatedToMe) {
            // 删除旧的存点通知，保持包厢窗口整洁
            if (isAutoSave) {
                lastAutosaveNode?.remove();
                lastAutosaveNode = div;
            }
            if (renderBatch) {
                renderBatch.privateFragment.appendChild(div);
                renderBatch.shouldPrunePrivate = true;
                renderBatch.shouldScrollPrivate = renderBatch.shouldScrollPrivate || autoScroll;
                return;
            }
            container2.appendChild(div);
            pruneMessageContainer(container2, PRIVATE_MESSAGE_NODE_LIMIT);
            if (autoScroll) {
                container2.scrollTop = container2.scrollHeight;
            }
        } else {
            if (renderBatch) {
                renderBatch.publicFragment.appendChild(div);
                renderBatch.shouldPrunePublic = true;
                renderBatch.shouldScrollPublic = renderBatch.shouldScrollPublic || autoScroll;
                return;
            }
            container.appendChild(div);
            pruneMessageContainer(container, PUBLIC_MESSAGE_NODE_LIMIT);
            scrollToBottom();
        }
    }

    /**
     * 裁剪聊天窗口内过旧的消息节点，防止长时间在线后 DOM 无限膨胀。
     *
     * @param {HTMLElement} targetContainer 聊天窗口容器
     * @param {number} maxNodes 最大保留节点数
     */
    function pruneMessageContainer(targetContainer, maxNodes) {
        if (!targetContainer || targetContainer.childElementCount <= maxNodes) {
            return;
        }

        while (targetContainer.childElementCount > maxNodes) {
            const firstNode = targetContainer.firstElementChild;
            if (firstNode === lastAutosaveNode) {
                lastAutosaveNode = null;
            }
            firstNode?.remove();
        }
    }

    /**
     * 创建聊天消息批量渲染上下文，集中提交 DOM 变更。
     *
     * @returns {Object}
     */
    function createChatMessageRenderBatch() {
        return {
            publicFragment: document.createDocumentFragment(),
            privateFragment: document.createDocumentFragment(),
            shouldPrunePublic: false,
            shouldPrunePrivate: false,
            shouldScrollPublic: false,
            shouldScrollPrivate: false,
        };
    }

    /**
     * 提交批量渲染的聊天消息，并把裁剪和滚动合并到每批一次。
     *
     * @param {Object} renderBatch 批量渲染上下文
     */
    function commitChatMessageRenderBatch(renderBatch) {
        const hasPublicMessages = renderBatch.publicFragment.childNodes.length > 0;
        const hasPrivateMessages = renderBatch.privateFragment.childNodes.length > 0;

        if (hasPublicMessages) {
            container.appendChild(renderBatch.publicFragment);
        }
        if (hasPrivateMessages) {
            container2.appendChild(renderBatch.privateFragment);
        }
        if (renderBatch.shouldPrunePublic) {
            pruneMessageContainer(container, PUBLIC_MESSAGE_NODE_LIMIT);
        }
        if (renderBatch.shouldPrunePrivate) {
            pruneMessageContainer(container2, PRIVATE_MESSAGE_NODE_LIMIT);
        }
        if (renderBatch.shouldScrollPublic) {
            container.scrollTop = container.scrollHeight;
        }
        if (renderBatch.shouldScrollPrivate) {
            container2.scrollTop = container2.scrollHeight;
        }
    }

    /**
     * 将广播消息放入轻量队列，避免连续广播时同步挤占同一帧的 DOM 渲染。
     */
    function enqueueChatMessage(msg) {
        // 本地清屏依赖最大消息 ID，需要在进入队列时先同步，避免延后渲染导致状态滞后。
        if (msg && msg.id > _maxMsgId) {
            _maxMsgId = msg.id;
        }

        pendingChatMessages.push(msg);

        if (chatMessageFlushTimer !== null) {
            return;
        }

        const scheduleFlush = window.requestAnimationFrame || ((callback) => window.setTimeout(callback, 16));
        chatMessageFlushTimer = scheduleFlush(flushQueuedChatMessages);
    }

    /**
     * 分批渲染待处理消息，给动画、输入和滚动留出主线程时间。
     */
    function flushQueuedChatMessages() {
        chatMessageFlushTimer = null;

        const batch = pendingChatMessages.splice(0, CHAT_MESSAGE_FLUSH_BATCH_SIZE);
        const renderBatch = createChatMessageRenderBatch();
        batch.forEach((msg) => appendMessage(msg, renderBatch));
        commitChatMessageRenderBatch(renderBatch);

        if (pendingChatMessages.length === 0) {
            return;
        }

        const scheduleFlush = window.requestAnimationFrame || ((callback) => window.setTimeout(callback, 16));
        chatMessageFlushTimer = scheduleFlush(flushQueuedChatMessages);
    }

    /**
     * 将消息追加函数暴露到全局，供页面首次加载时回填历史消息使用。
     */
    window.appendMessage = appendMessage;

    // ── WebSocket 初始化 ─────────────────────────────
    document.addEventListener('DOMContentLoaded', () => {
        if (typeof window.initChat === 'function') {
            window.initChat(window.chatContext.roomId);
        }

        // 手机端：在公屏(say1)和包厢(say2)容器上注册 touchend 委托
        // 检测 300ms 内两次触摸同一 [data-u] 用户名 span，触发 openUserCard
        let _msgTapTarget = null;
        let _msgTapTime   = 0;
        ['say1', 'say2'].forEach(id => {
            const el = document.getElementById(id);
            if (!el) { return; }
            el.addEventListener('touchend', (e) => {
                const span = e.target.closest('[data-u]');
                if (!span) { return; }
                const uName = span.dataset.u;
                const now   = Date.now();
                if (uName === _msgTapTarget && now - _msgTapTime < 300) {
                    e.preventDefault();
                    openUserCard(uName);
                    _msgTapTarget = null;
                    _msgTapTime   = 0;
                } else {
                    _msgTapTarget = uName;
                    _msgTapTime   = now;
                }
            }, { passive: false });
        });
    });

    // ── WebSocket 事件监听 ────────────────────────────
    window.addEventListener('chat:here', (e) => {
        const users = e.detail;
        onlineUsers = {};
        // onlineUsers 使用 let 重建对象时，需要同步 window 引用给 Vite 模块。
        window.onlineUsers = onlineUsers;
        users.forEach(u => {
            hydrateOnlineUserPayload(u.username, u);
        });

        // 初始加载时，如果全局且开启，注入 AI
        if (window.chatContext.chatBotEnabled && window.chatContext.botUser) {
            hydrateOnlineUserPayload('AI小班长', window.chatContext.botUser);
        }

        setOnlineUserDailyStatus(window.chatContext.username, getCurrentUserDailyStatus());
        syncDailyStatusUi();
        scheduleRenderUserList(0);
    });

    // 监听机器人动态开关
    window.addEventListener('chat:bot-toggled', (e) => {
        const detail = e.detail;
        window.chatContext.chatBotEnabled = detail.isOnline;
        
        if (detail.isOnline && detail.user && detail.user.username) {
            hydrateOnlineUserPayload(detail.user.username, detail.user);
            window.chatContext.botUser = detail.user;
        } else {
            delete onlineUsers['AI小班长'];
            window.chatContext.botUser = null;
        }
        scheduleRenderUserList();
    });

    window.addEventListener('chat:user-status-updated', (e) => {
        const username = e.detail?.username;
        const payload = e.detail?.user;

        if (!username || !payload) {
            return;
        }

        hydrateOnlineUserPayload(username, payload);

        if (username === window.chatContext.username) {
            window.chatContext.currentDailyStatus = normalizeDailyStatus(payload);
            syncDailyStatusUi();
        }

        scheduleRenderUserList();
    });

    window.addEventListener('chat:joining', (e) => {
        const user = e.detail;
        hydrateOnlineUserPayload(user.username, user);
        scheduleRenderUserList();
    });

    window.addEventListener('chat:leaving', (e) => {
        const user = e.detail;
        delete onlineUsers[user.username];
        scheduleRenderUserList();
    });

    window.addEventListener('chat:message', (e) => {
        const msg = e.detail;
        if (msg.is_secret && msg.from_user !== window.chatContext.username && msg.to_user !== window
            .chatContext.username) {
            return;
        }
        enqueueChatMessage(msg);

        if (msg.action === 'vip_presence') {
            showVipPresenceBanner(msg);
        }

        // 若消息携带 toast_notification 字段且当前用户是接收者，弹右下角小卡片
        if (msg.toast_notification && msg.to_user === window.chatContext.username) {
            const t = msg.toast_notification;
            window.chatToast.show({
                title: t.title || '通知',
                message: t.message || '',
                icon: t.icon || '💬',
                color: t.color || '#336699',
                duration: t.duration ?? 8000,
            });
        }
    });

    window.addEventListener('chat:kicked', (e) => {
        if (e.detail.username === window.chatContext.username) {
            window.chatDialog.alert('您已被管理员踢出房间！' + (e.detail.reason ? ' 原因：' + e.detail.reason : ''), '系统通知',
                '#cc4444');
            window.location.href = "{{ route('rooms.index') }}";
        }
    });

    // ── 禁言状态（本地计时器） ──
    let isMutedUntil = 0;

    window.addEventListener('chat:muted', (e) => {
        const d = e.detail;
        const now = new Date();
        const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
            now.getMinutes().toString().padStart(2, '0') + ':' +
            now.getSeconds().toString().padStart(2, '0');

        const isMe = d.username === window.chatContext.username;

        // 禁言通知：自己被禁言显示在包厢(say2)，其他人显示在公聊(say1)
        const div = document.createElement('div');
        div.className = 'msg-line';
        div.innerHTML =
            `<span style="color: #c00; font-weight: bold;">【系统】${d.message}</span><span class="msg-time">(${timeStr})</span>`;
        const targetContainer = isMe ? document.getElementById('say2') : container;
        if (targetContainer) {
            targetContainer.appendChild(div);
            targetContainer.scrollTop = targetContainer.scrollHeight;
        }

        // 如果是自己被禁言，设置本地禁言计时
        if (isMe && d.mute_time > 0) {
            isMutedUntil = Date.now() + d.mute_time * 60 * 1000;
            const contentInput = document.getElementById('content');
            const operatorName = d.operator || '管理员';
            if (contentInput) {
                contentInput.placeholder = `${operatorName} 已将您禁言 ${d.mute_time} 分钟，解禁后方可发言...`;
                contentInput.disabled = true;
                // 到期自动恢复
                setTimeout(() => {
                    isMutedUntil = 0;
                    contentInput.placeholder = '在这里输入聊天内容，按 Enter 发送...';
                    contentInput.disabled = false;
                    const unmuteDiv = document.createElement('div');
                    unmuteDiv.className = 'msg-line';
                    unmuteDiv.innerHTML =
                        '<span style="color: #16a34a; font-weight: bold;">【系统】您的禁言已解除，可以继续发言了。</span>';
                    // 解禁提示也显示在包厢窗口
                    const say2 = document.getElementById('say2');
                    if (say2) {
                        say2.appendChild(unmuteDiv);
                        say2.scrollTop = say2.scrollHeight;
                    }
                }, d.mute_time * 60 * 1000);
            }
        }
    });

    window.addEventListener('chat:title-updated', (e) => {
        document.getElementById('room-title-display').innerText = e.detail.title;
    });

    /**
     * 收到站长的全员刷新通知后，先弹出提示，再延迟刷新页面。
     */
    window.addEventListener('chat:browser-refresh-requested', (e) => {
        const detail = e.detail || {};
        const operatorName = escapeHtml(String(detail.operator || '站长'));
        const reasonText = escapeHtml(String(detail.reason || '页面功能已更新，请重新载入。'));

        window.chatToast?.show({
            title: '页面即将刷新',
            message: `<b>${operatorName}</b> 通知全员刷新页面。<br><span style="color:#475569;">${reasonText}</span>`,
            icon: '♻️',
            color: '#0f766e',
            duration: 2200,
        });

        window.setTimeout(() => {
            window.location.reload();
        }, 900);
    });

    /**
     * 任命/撤职后，目标用户收到定向刷新通知，自动同步页面上的权限按钮。
     */
    window.addEventListener('chat:user-browser-refresh-requested', (e) => {
        const detail = e.detail || {};
        const operatorName = escapeHtml(String(detail.operator || '管理员'));
        const reasonText = escapeHtml(String(detail.reason || '你的权限状态已发生变化，页面即将刷新。'));

        window.chatToast?.show({
            title: '权限同步中',
            message: `<b>${operatorName}</b> 已更新你的职务状态。<br><span style="color:#475569;">${reasonText}</span>`,
            icon: '🔄',
            color: '#7c3aed',
            duration: 2600,
        });

        window.setTimeout(() => {
            window.location.reload();
        }, 1000);
    });

    // ── 管理员全员清屏事件（等待 Echo 就绪后监听） ───────
    function setupScreenClearedListener() {
        if (!window.Echo || !window.chatContext) {
            // Echo 或 chatContext 还没就绪，延迟重试
            setTimeout(setupScreenClearedListener, 500);
            return;
        }
        window.Echo.join(`room.${window.chatContext.roomId}`)
            .listen('ScreenCleared', (e) => {
                console.log('收到全员清屏事件:', e);
                const operator = e.operator;
                const safeOperator = escapeHtml(String(operator || ''));

                // 清除公聊窗口所有消息
                const say1 = document.getElementById('chat-messages-container');
                if (say1) say1.innerHTML = '';

                // 清除包厢窗口中非悄悄话的消息
                const say2 = document.getElementById('chat-messages-container2');
                if (say2) {
                    const items = say2.querySelectorAll('.msg-line');
                    items.forEach(item => {
                        // 保留悄悄话消息（含 msg-secret 类）
                        if (!item.querySelector('.msg-secret')) {
                            item.remove();
                        }
                    });
                }
                lastAutosaveNode = say2?.querySelector('[data-autosave="1"]') || null;

                // 显示清屏提示
                const sysDiv = document.createElement('div');
                sysDiv.className = 'msg-line';
                const now = new Date();
                const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
                    now.getMinutes().toString().padStart(2, '0') + ':' +
                    now.getSeconds().toString().padStart(2, '0');
                sysDiv.innerHTML =
                    `<span style="color: #dc2626; font-weight: bold;">🧹 管理员 <b>${safeOperator}</b> 已执行全员清屏</span><span class="msg-time">(${timeStr})</span>`;
                if (say1) {
                    say1.appendChild(sysDiv);
                    say1.scrollTop = say1.scrollHeight;
                }
            });
        console.log('ScreenCleared 监听器已注册');
    }
    // DOMContentLoaded 后启动，确保 Vite 编译的 JS 已加载
    document.addEventListener('DOMContentLoaded', setupScreenClearedListener);

    /**
     * 注册房间级“刷新全员”监听。
     *
     * 放在 Blade 脚本内，避免前端资源未重新构建时收不到刷新广播。
     */
    function setupRoomBrowserRefreshListener() {
        if (!window.Echo || !window.chatContext) {
            setTimeout(setupRoomBrowserRefreshListener, 500);
            return;
        }

        window.Echo.join(`room.${window.chatContext.roomId}`)
            .listen('BrowserRefreshRequested', (e) => {
                console.log('收到全员刷新事件:', e);
                window.dispatchEvent(
                    new CustomEvent('chat:browser-refresh-requested', {
                        detail: e
                    })
                );
            });

        console.log('BrowserRefreshRequested 监听器已注册');
    }

    document.addEventListener('DOMContentLoaded', setupRoomBrowserRefreshListener);

    // ── 开发日志发布通知（仅 Room 1 大厅可见）────────────
    /**
     * 监听 ChangelogPublished 事件，在大厅聊天区展示系统通知
     * 通知包含版本号、标题和可点击的查看链接
     * 复用「系统传音」样式（金色左边框，不喧宾夺主）
     */
    function setupChangelogPublishedListener() {
        if (!window.Echo || !window.chatContext) {
            setTimeout(setupChangelogPublishedListener, 500);
            return;
        }
        // 仅在 Room 1（星光大厅）时监听
        if (window.chatContext.roomId !== 1) {
            return;
        }
        window.Echo.join('room.1')
            .listen('.ChangelogPublished', (e) => {
                const now = new Date();
                const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
                    now.getMinutes().toString().padStart(2, '0') + ':' +
                    now.getSeconds().toString().padStart(2, '0');
                const safeVersion = e.safe_version ?? escapeHtml(String(e.version ?? ''));
                const safeTitle = e.safe_title ?? escapeHtml(String(e.title ?? ''));
                const safeUrl = escapeHtml(normalizeSafeChatUrl(e.url, '{{ route('changelog.index') }}'));

                const sysDiv = document.createElement('div');
                sysDiv.className = 'msg-line';
                // 金色左边框通知样式（复用「系统传音」风格）
                sysDiv.style.cssText =
                    'background: #fffbeb; border-left: 3px solid #d97706; border-radius: 4px; padding: 5px 10px; margin: 3px 0;';
                sysDiv.innerHTML = `<span style="color: #b45309; font-weight: bold;">
                    📋 【版本更新】v${safeVersion} · ${safeTitle}
                    <a href="${safeUrl}" target="_blank" rel="noopener"
                        style="color: #7c3aed; text-decoration: underline; margin-left: 8px; font-size: 0.85em;">
                        查看详情 →
                    </a>
                </span><span class="msg-time">(${timeStr})</span>`;

                const say1 = document.getElementById('chat-messages-container');
                if (say1) {
                    say1.appendChild(sysDiv);
                    say1.scrollTop = say1.scrollHeight;
                }
            });
        console.log('ChangelogPublished 监听器已注册（Room 1 专属）');
    }
    document.addEventListener('DOMContentLoaded', setupChangelogPublishedListener);

    // ── 五子棋 PvP 邀请通知（聊天室内显示「接受挑战」按钮）───────
    /**
     * 监听 .gomoku.invite 事件，在聊天窗口追加邀请消息行。
     * 发起者收到的邀请（自己发出的）不显示接受按钮。
     */
    function setupGomokuInviteListener() {
        if (!window.Echo || !window.chatContext) {
            setTimeout(setupGomokuInviteListener, 500);
            return;
        }
        window.Echo.join(`room.${window.chatContext.roomId}`)
            .listen('.gomoku.invite', (e) => {
                const now = new Date();
                const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
                    now.getMinutes().toString().padStart(2, '0') + ':' +
                    now.getSeconds().toString().padStart(2, '0');

                const isSelf = (e.inviter_name === window.chatContext.username);
                const div = document.createElement('div');
                div.className = 'msg-line';
                div.style.cssText =
                    'background:linear-gradient(135deg,#e8eef8,#f0f4fc); ' +
                    'border-left:3px solid #336699; border-radius:4px; padding:6px 10px; margin:3px 0;';

                const acceptBtn = isSelf ?
                    // 自己的邀请：只显示打开面板按钮，方便被关掉后重新进入
                    `<button onclick="document.querySelector('[x-data=\"gomokuPanel()\"]').__x.$data.open()" 
                           style="margin-left:10px; padding:3px 12px; border:1.5px solid #2d6096;
                                  border-radius:12px; background:#f0f6ff; color:#2d6096; font-size:12px;
                                  cursor:pointer; font-family:inherit; transition:all .15s;"
                           onmouseover="this.style.background='#ddeeff'" onmouseout="this.style.background='#f0f6ff'">
                           ⤴️ 打开面板
                       </button>` :
                    // 别人的邀请：显示接受挑战按钮
                    `<button onclick="acceptGomokuInvite(${e.game_id})" id="gomoku-accept-${e.game_id}"
                           style="margin-left:10px; padding:3px 12px; border:1.5px solid #336699;
                                  border-radius:12px; background:#336699; color:#fff; font-size:12px;
                                  cursor:pointer; font-family:inherit; transition:all .15s;"
                           onmouseover="this.style.opacity='.8'" onmouseout="this.style.opacity='1'">
                           ⚔️ 接受挑战
                       </button>`;

                div.innerHTML = `<span style="color:#1e3a5f; font-weight:bold;">
                        ♟️ 【五子棋】<b>${e.inviter_name}</b> 发起了随机对战！${isSelf ? '（等待中）' : ''}
                    </span>${acceptBtn}
                    <span class="msg-time">(${timeStr})</span>`;

                // 追加到公聊窗口
                const say1 = document.getElementById('chat-messages-container');
                if (say1) {
                    say1.appendChild(div);
                    say1.scrollTop = say1.scrollHeight;
                }

                // 60 秒后移除接受按钮（邀请超时）
                if (!isSelf) {
                    setTimeout(() => {
                        const btn = document.getElementById(`gomoku-accept-${e.game_id}`);
                        if (btn) {
                            btn.textContent = '已超时';
                            btn.disabled = true;
                            btn.style.opacity = '.5';
                            btn.style.cursor = 'not-allowed';
                        }
                    }, 60000);
                }
            })
            .listen('.gomoku.finished', (e) => {
                // 对局结束：在公聊展示战报（仅 PvP 有战报意义）
                if (e.mode !== 'pvp') return;
                const now = new Date();
                const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
                    now.getMinutes().toString().padStart(2, '0') + ':' +
                    now.getSeconds().toString().padStart(2, '0');

                const div = document.createElement('div');
                div.className = 'msg-line';
                div.style.cssText =
                    'background:#fffae8; border-left:3px solid #d97706; border-radius:4px; padding:5px 10px; margin:2px 0;';

                const reason = {
                    win: '获胜',
                    draw: '平局',
                    resign: '认输',
                    timeout: '超时'
                } [e.reason] || '结束';
                let text = '';
                if (e.winner === 0) {
                    text = `♟️ 五子棋对局以<b>平局</b>结束！`;
                } else {
                    text =
                        `♟️ <b>${e.winner_name}</b> 击败 <b>${e.loser_name}</b>（${reason}）获得 <b style="color:#b45309;">${e.reward_gold}</b> 金币！`;
                }

                div.innerHTML =
                    `<span style="color:#92400e;">${text}</span><span class="msg-time">(${timeStr})</span>`;
                const say1 = document.getElementById('chat-messages-container');
                if (say1) {
                    say1.appendChild(div);
                    say1.scrollTop = say1.scrollHeight;
                }
            });
        console.log('[五子棋] 邀请监听器已注册');
    }
    document.addEventListener('DOMContentLoaded', setupGomokuInviteListener);

    // ── 全屏特效事件监听（管理员菜单 / 会员进出场通用）─────────
    window.addEventListener('chat:effect', (e) => {
        const type = e.detail?.type;
        const target = e.detail?.target_username; // null = 全员，otherwise 指定昵称
        const operator = e.detail?.operator; // 定向赠送时，购买者自己也应能看到特效
        const myName = window.chatContext?.username;

        // null 表示全员；若有指定接收者，则购买者本人和指定用户都播放
        if (type && typeof EffectManager !== 'undefined') {
            if (!target || target === myName || operator === myName) {
                EffectManager.play(type);
            }
        }
    });

    /**
     * 管理员点击特效按钮，向后端 POST /command/effect
     *
     * @param {string} type  特效类型：fireworks / rain / lightning / snow / sakura / meteors / gold-rain / hearts / confetti / fireflies
     */
    function triggerEffect(type) {
        const roomId = window.chatContext?.roomId;
        if (!roomId) return;
        fetch('/command/effect', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
            },
            body: JSON.stringify({
                room_id: roomId,
                type
            }),
        }).then(r => r.json()).then(data => {
            if (data.status !== 'success') window.chatDialog.alert(data.message, '操作失败', '#cc4444');
        }).catch(err => console.error('特效触发失败:', err));
    }
    window.toggleAdminMenu = toggleAdminMenu;
    window.toggleBlockMenu = toggleBlockMenu;
    window.toggleFeatureMenu = toggleFeatureMenu;
    window.closeFeatureMenu = closeFeatureMenu;
    window.openDailyStatusEditor = openDailyStatusEditor;
    window.closeDailyStatusEditor = closeDailyStatusEditor;
    window.runAdminAction = runAdminAction;
    window.selectEffect = selectEffect;
    window.triggerEffect = triggerEffect;
    window.toggleBlockedSystemSender = toggleBlockedSystemSender;
    window.updateDailyStatus = updateDailyStatus;
    window.clearDailyStatus = clearDailyStatus;
    window.handleFeatureLocalClear = handleFeatureLocalClear;
    syncDailyStatusUi();

    // 页面加载后从 localStorage 恢复之前保存的字号
    document.addEventListener('DOMContentLoaded', () => {
        window.ChatRoomTools?.restoreChatFontSize?.();

        const storedBlockedSystemSenders = loadBlockedSystemSenders();
        const mutedFromLocal = localStorage.getItem(CHAT_SOUND_MUTED_STORAGE_KEY) === '1';
        const shouldMigrateLocalPreferences = window.ChatRoomTools?.shouldMigrateLocalChatPreferences
            ? window.ChatRoomTools.shouldMigrateLocalChatPreferences(initialChatPreferences, storedBlockedSystemSenders, mutedFromLocal)
            : !(initialChatPreferences.blocked_system_senders.length > 0 || initialChatPreferences.sound_muted)
                && (storedBlockedSystemSenders.length > 0 || mutedFromLocal);

        if (shouldMigrateLocalPreferences) {
            blockedSystemSenders = new Set(storedBlockedSystemSenders);
        }

        // 恢复禁音复选框状态；默认一律为未禁音。
        const muted = shouldMigrateLocalPreferences ? mutedFromLocal : initialChatPreferences.sound_muted;
        if (window.ChatRoomTools?.setSoundMuted) {
            window.ChatRoomTools.setSoundMuted(muted);
        } else {
            const muteChk = document.getElementById('sound_muted');
            if (muteChk) muteChk.checked = muted;
        }
        syncBlockedSystemSenderCheckboxes();

        if (shouldMigrateLocalPreferences) {
            void saveChatPreferences();
        } else {
            persistChatPreferencesToLocal();
        }
    });

    // ── 特效禁音开关 ─────────────────────────────────────────────────
    /**
     * 切换特效音效的静音状态，持久化到 localStorage。
     * 开启禁音后立即停止当前正在播放的音效。
     *
     * @param {boolean} muted  true = 禁音，false = 开启声音
     */
    function toggleSoundMute(muted) {
        if (window.ChatRoomTools?.setSoundMuted) {
            window.ChatRoomTools.setSoundMuted(muted);
        } else {
            localStorage.setItem(CHAT_SOUND_MUTED_STORAGE_KEY, muted ? '1' : '0');
        }
        if (muted && typeof EffectSounds !== 'undefined') {
            EffectSounds.stop(); // 立即停止当前音效
        }

        void saveChatPreferences();
    }
    window.toggleSoundMute = toggleSoundMute;
    window.ChatRoomTools?.bindSoundMuteControl?.(() => saveChatPreferences());


    // ── 发送消息（Enter 发送，防 IME 输入法重复触发）────────
    // 用 isComposing 标记中文输入法的组词状态，组词期间过滤掉 Enter
    let _imeComposing = false;
    let _isSending = false; // 发送中防重入标记
    let _sendStartedAt = 0; // 记录发送开始时间，用于页面恢复后释放异常锁
    const _contentInput = document.getElementById('content');
    const CHAT_DRAFT_STORAGE_KEY = `chat_draft_${window.chatContext?.roomId ?? '0'}_${window.chatContext?.userId ?? '0'}`;

    /**
     * 将当前输入框内容保存到会话级草稿缓存。
     */
    function persistChatDraft(value = null) {
        try {
            const draft = value ?? _contentInput?.value ?? '';
            if (draft === '') {
                sessionStorage.removeItem(CHAT_DRAFT_STORAGE_KEY);
                return;
            }

            sessionStorage.setItem(CHAT_DRAFT_STORAGE_KEY, draft);
        } catch (_) {
            // 会话存储不可用时静默降级，不影响聊天主流程
        }
    }

    /**
     * 从会话缓存中恢复聊天草稿。
     *
     * @returns {string}
     */
    function loadChatDraft() {
        try {
            return sessionStorage.getItem(CHAT_DRAFT_STORAGE_KEY) || '';
        } catch (_) {
            return '';
        }
    }

    /**
     * 将当前输入区状态整理为一份稳定快照，避免直接序列化整张表单。
     *
     * @returns {Object}
     */
    function collectChatComposerState() {
        const contentInput = document.getElementById('content');
        const submitBtn = document.getElementById('send-btn');
        const imageInput = document.getElementById('chat_image');
        const toUserSelect = document.getElementById('to_user');
        const actionSelect = document.getElementById('action');
        const fontColorInput = document.getElementById('font_color');
        const secretCheckbox = document.getElementById('is_secret');
        const contentRaw = contentInput?.value ?? '';
        const selectedImage = imageInput?.files?.[0] ?? null;

        return {
            contentInput,
            submitBtn,
            imageInput,
            contentRaw,
            content: contentRaw.trim(),
            selectedImage,
            toUser: toUserSelect?.value || '大家',
            action: actionSelect?.value || '',
            fontColor: fontColorInput?.value || '',
            isSecret: Boolean(secretCheckbox?.checked),
        };
    }

    /**
     * 基于当前聊天快照构造稳定的 multipart 请求体。
     *
     * @param {Object} composerState
     * @returns {FormData}
     */
    function buildChatMessageFormData(composerState) {
        const formData = new FormData();
        formData.append('content', composerState.contentRaw);
        formData.append('to_user', composerState.toUser);
        formData.append('action', composerState.action);
        formData.append('font_color', composerState.fontColor);

        if (composerState.isSecret) {
            formData.append('is_secret', '1');
        }

        if (composerState.selectedImage) {
            formData.append('image', composerState.selectedImage);
        }

        return formData;
    }

    /**
     * 处理聊天图片选择后的前端状态展示。
     */
    function handleChatImageSelected(input) {
        const file = input?.files?.[0] ?? null;
        if (!file) {
            return;
        }

        // 用户选择图片后，立即触发自动发送
        sendMessage(null);
    }

    /**
     * 清理当前选中的聊天图片。
     */
    function clearSelectedChatImage(resetInput = false) {
        const imageInput = document.getElementById('chat_image');

        if (resetInput && imageInput) {
            imageInput.value = '';
        }
    }

    /**
     * 页面从后台恢复后，同步草稿、图片提示和发送锁状态。
     */
    function syncChatComposerAfterResume() {
        if (!_contentInput) {
            return;
        }

        const savedDraft = loadChatDraft();
        if (_contentInput.value === '' && savedDraft !== '') {
            _contentInput.value = savedDraft;
        } else if (_contentInput.value !== '') {
            persistChatDraft(_contentInput.value);
        }

        const imageInput = document.getElementById('chat_image');
        if (!imageInput?.files?.length) {
            clearSelectedChatImage();
        }

        _imeComposing = false;

        if (_isSending && Date.now() - _sendStartedAt > 15000) {
            const submitBtn = document.getElementById('send-btn');
            if (submitBtn) {
                submitBtn.disabled = false;
            }

            _isSending = false;
            _sendStartedAt = 0;
        }
    }

    window.handleChatImageSelected = handleChatImageSelected;
    syncChatComposerAfterResume();

    if (_contentInput) {
        _contentInput.addEventListener('input', function() {
            persistChatDraft(this.value);
        });

        // 中文/日文等 IME 组词开始
        _contentInput.addEventListener('compositionstart', () => {
            _imeComposing = true;
        });
        // 组词结束（确认候选词完成），给 10ms 缓冲让 keydown 先被过滤掉
        _contentInput.addEventListener('compositionend', () => {
            setTimeout(() => {
                _imeComposing = false;
            }, 10);
        });

        _contentInput.addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                // IME 正在组词时（如选候选汉字），不触发发送
                if (_imeComposing) return;
                sendMessage(e);
            }
        });
    }

    window.addEventListener('pageshow', syncChatComposerAfterResume);
    document.addEventListener('visibilitychange', function() {
        if (document.visibilityState === 'visible') {
            syncChatComposerAfterResume();
        }
    });
    window.addEventListener('focus', function() {
        setTimeout(syncChatComposerAfterResume, 0);
    });

    /**
     * 发送聊天消息（内带防重入锁，避免快速连按 Enter 重复提交）
     */
    async function sendMessage(e) {
        if (e) e.preventDefault();
        if (_isSending) return; // 上一次还没结束，忽略
        _isSending = true;
        _sendStartedAt = Date.now();

        // 前端禁言检查
        if (isMutedUntil > Date.now()) {
            const remaining = Math.ceil((isMutedUntil - Date.now()) / 1000);
            const remainMin = Math.ceil(remaining / 60);
            // 在聊天窗口显示持久提示，避免弹窗消失太快
            const muteDiv = document.createElement('div');
            muteDiv.className = 'msg-line';
            muteDiv.innerHTML =
                `<span style="color: #dc2626; font-weight: bold;">【提示】您正在禁言中，还需等待约 ${remainMin} 分钟（${remaining} 秒）后方可发言。</span>`;
            const container2 = document.getElementById('say2');
            if (container2) {
                container2.appendChild(muteDiv);
                container2.scrollTop = container2.scrollHeight;
            }
            _isSending = false;
            _sendStartedAt = 0;
            return;
        }

        const composerState = collectChatComposerState();
        const {
            contentInput,
            submitBtn,
            content,
            contentRaw,
            selectedImage,
            toUser,
        } = composerState;

        if (!content && !selectedImage) {
            contentInput.focus();
            _isSending = false;
            _sendStartedAt = 0;
            return;
        }

        // 如果发言对象是 AI 小助手，也发送一份给专用机器人 API，不打断正常的发消息流程
        if (toUser === 'AI小班长' && content) {
            sendToChatBot(content, composerState.isSecret); // 异步调用，私聊状态一并传递
        }

        // ── 神秘箱子暗号拦截 ────────────────────────────────────
        // 当用户输入内容符合暗号格式（4-8位大写字母/数字）时，主动尝试领取
        // 不依赖轮询标志，服务端负责校验是否真有活跃箱子及暗号是否匹配
        const passcodePattern = /^[A-Z0-9]{4,8}$/;
        if (!selectedImage && passcodePattern.test(content.trim())) {
            _isSending = false;
            _sendStartedAt = 0;

            try {
                const claimRes = await fetch('/mystery-box/claim', {
                    method: 'POST',
                    headers: {
                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
                        'Content-Type': 'application/json',
                        'Accept': 'application/json',
                    },
                    body: JSON.stringify({
                        passcode: content.trim()
                    }),
                });
                const claimData = await claimRes.json();

                if (claimData.ok) {
                    // ✅ 领取成功：清空输入框，不发送普通消息
                    contentInput.value = '';
                    persistChatDraft('');
                    contentInput.focus();

                    // 清除活跃箱子全局标志
                    window._mysteryBoxActive = false;
                    window._mysteryBoxPasscode = null;

                    // 弹出开箱结果卡片
                    const isPositive = (claimData.reward ?? 1) >= 0;
                    window.chatDialog?.alert(
                        claimData.message || '开箱成功！',
                        isPositive ? '🎉 恭喜！' : '☠️ 中了陷阱！',
                        isPositive ? '#10b981' : '#ef4444',
                    );

                    // 更新全局金币余额显示
                    if (window.__chatUser && claimData.balance !== undefined) {
                        window.__chatUser.jjb = claimData.balance;
                    }

                    return;
                }

                // ❌ 领取失败（暗号错误 / 无活跃箱子 / 已被领走）
                // 静默回退到正常发送——不弹错误提示，让消息正常发出
            } catch (_) {
                // 网络错误时同样静默回退正常发送
            }
        }

        submitBtn.disabled = true;
        const formData = buildChatMessageFormData({
            ...composerState,
            contentRaw,
        });

        try {
            const response = await fetch(window.chatContext.sendUrl, {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
                        'content'),
                    'Accept': 'application/json'
                },
                body: formData
            });

            const data = await response.json();
            if (response.ok && data.status === 'success') {
                contentInput.value = '';
                persistChatDraft('');
                clearSelectedChatImage(true);
                contentInput.focus();
            } else {
                window.chatDialog.alert('发送失败: ' + (data.message || JSON.stringify(data.errors)), '操作失败',
                    '#cc4444');
            }
        } catch (error) {
            window.chatDialog.alert('网络连接错误，消息发送失败！', '网络错误', '#cc4444');
            console.error(error);
        } finally {
            submitBtn.disabled = false;
            _isSending = false; // 释放发送锁，允许下次发送
            _sendStartedAt = 0;
        }
    }


    // ── 设置房间公告 ─────────────────────────────────────
    function promptAnnouncement() {
        // 从 marquee 读取当前公告全文，剥离末尾的「——发送者 日期」元信息，仅预填纯内容
        const fullText = document.getElementById('announcement-text')?.textContent?.trim() || '';
        const pureText = fullText.replace(/ ——\S+ \d{2}-\d{2} \d{2}:\d{2}$/, '').trim();
        // 使用全局弹窗替代原生 prompt()，通过 .then() 注册回调确保事件正确触发
        window.chatDialog.prompt('请输入新的房间公告/祝福语：', pureText, '设置公告', '#336699').then(newText => {
            if (newText === null || newText.trim() === '') return;

            fetch(`/room/${window.chatContext.roomId}/announcement`, {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
                        'content'),
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                },
                body: JSON.stringify({
                    announcement: newText.trim()
                })
            }).then(res => res.json()).then(data => {
                if (data.status === 'success') {
                    // 用后端返回的含发送者+时间的完整公告文本更新滚动条
                    const marquee = document.getElementById('announcement-text');
                    if (marquee) marquee.textContent = data.announcement;
                    window.chatDialog.alert('公告已更新！', '提示', '#16a34a');
                } else {
                    window.chatDialog.alert(data.message || '更新失败', '操作失败', '#cc4444');
                }
            }).catch(e => {
                window.chatDialog.alert('设置公告失败：' + e.message, '操作失败', '#cc4444');
            });
        });
    }

    // ── 站长公屏讲话 ─────────────────────────────────────
    function promptAnnounceMessage() {
        // 使用全局弹窗替代原生 prompt()，通过 .then() 注册回调确保事件正确触发
        window.chatDialog.prompt('请输入公屏讲话内容：', '', '📢 公屏讲话', '#7c3aed').then(content => {
            if (!content || !content.trim()) return;

            fetch('/command/announce', {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
                        'content'),
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                },
                body: JSON.stringify({
                    content: content.trim(),
                    room_id: window.chatContext.roomId,
                })
            }).then(res => res.json()).then(data => {
                if (data.status !== 'success') {
                    window.chatDialog.alert(data.message || '发送失败', '操作失败', '#cc4444');
                }
            }).catch(e => {
                window.chatDialog.alert('发送失败：' + e.message, '操作失败', '#cc4444');
            });
        });
    }

    // ── 管理员全员清屏 ─────────────────────────────────────
    function adminClearScreen() {
        // 使用全局弹窗替代原生 confirm()，通过 .then() 注册回调确保事件正确触发
        window.chatDialog.confirm('确定要清除所有人的聊天记录吗？（悄悄话将保留）', '全员清屏', '#dc2626').then(ok => {
            if (!ok) return;

            fetch('/command/clear-screen', {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
                        'content'),
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                },
                body: JSON.stringify({
                    room_id: window.chatContext.roomId,
                })
            }).then(res => res.json()).then(data => {
                if (data.status !== 'success') {
                    window.chatDialog.alert(data.message || '清屏失败', '操作失败', '#cc4444');
                }
            }).catch(e => {
                window.chatDialog.alert('清屏失败：' + e.message, '操作失败', '#cc4444');
            });
        });
    }

    // ── 本地清屏（仅限自己的屏幕）───────────────────────────

    function localClearScreen() {
        // 清理公聊窗口
        const say1 = document.getElementById('chat-messages-container');
        if (say1) say1.innerHTML = '';

        // 清理包厢窗口
        const say2 = document.getElementById('chat-messages-container2');
        if (say2) say2.innerHTML = '';
        lastAutosaveNode = null;

        // 将当前最大消息 ID 保存至本地，刷新时只显示大于此 ID 的历史记录
        localStorage.setItem(`local_clear_msg_id_${window.chatContext.roomId}`, _maxMsgId);

        // 插入清屏提示
        const sysDiv = document.createElement('div');
        sysDiv.className = 'msg-line';
        const now = new Date();
        const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2,
            '0') + ':' + now.getSeconds().toString().padStart(2, '0');
        sysDiv.innerHTML =
            `<span style="color: #64748b; font-weight: bold;">🧹 您已执行本地清屏</span><span class="msg-time">(${timeStr})</span>`;
        if (say1) {
            say1.appendChild(sysDiv);
            say1.scrollTop = say1.scrollHeight;
        }
    }

    /**
     * 在状态面板中触发本地清屏，并顺手关闭面板。
     */
    function handleFeatureLocalClear() {
        closeFeatureMenu();
        localClearScreen();
    }

    // ── 滚屏开关 ─────────────────────────────────────
    function toggleAutoScroll() {
        autoScroll = !autoScroll;
        const cb = document.getElementById('auto_scroll');
        if (cb) cb.checked = autoScroll;
        const statusEl = document.getElementById('scroll-status');
        if (statusEl) statusEl.textContent = autoScroll ? '开' : '关';
    }

    // ── 退出房间 ─────────────────────────────────────
    let leaveRequestInFlight = false;

    async function leaveRoom() {
        if (leaveRequestInFlight) {
            return;
        }

        leaveRequestInFlight = true;

        try {
            await fetch(window.chatContext.leaveUrl + '?explicit=1', {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
                        'content'),
                    'Accept': 'application/json'
                }
            });
        } catch (e) {
            console.error(e);
        }
        // 弹出窗口直接关闭，如果不是弹出窗口则跳回首页
        window.close();
        setTimeout(() => {
            window.location.href = '/';
        }, 500);
    }

    async function notifyExpiredLeave() {
        if (leaveRequestInFlight) {
            return;
        }

        leaveRequestInFlight = true;

        try {
            if (!window.chatContext?.expiredLeaveUrl) {
                return;
            }

            await fetch(window.chatContext.expiredLeaveUrl, {
                method: 'GET',
                headers: {
                    'Accept': 'application/json'
                },
                credentials: 'same-origin'
            });
        } catch (e) {
            console.error(e);
        }
    }


    // ── 掉线检测计数器 ──
    let heartbeatFailCount = 0;
    const MAX_HEARTBEAT_FAILS = 3;

    // ── 存点功能（手动 + 自动）─────────────────────
    async function saveExp(silent = false) {
        if (!window.chatContext || !window.chatContext.heartbeatUrl) return;
        try {
            const response = await fetch(window.chatContext.heartbeatUrl, {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')
                        .getAttribute('content'),
                    'Accept': 'application/json'
                }
            });

            // 检测登录态失效
            if (response.status === 401 || response.status === 419) {
                await notifyExpiredLeave();
                window.chatDialog.alert('⚠️ 您的登录已失效（可能超时或在其他设备登录），请重新登录。', '连接警告', '#b45309');
                window.location.href = '/';
                return;
            }

            const data = await response.json();
            if (response.ok && data.status === 'success') {
                heartbeatFailCount = 0;
                const now = new Date();
                const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
                    now.getMinutes().toString().padStart(2, '0') + ':' +
                    now.getSeconds().toString().padStart(2, '0');
                const d = data.data;
                const identitySummary = d.identity_summary ? `${d.identity_summary} · ` : '';

                let levelInfo = '';
                if (d.is_max_level) {
                    levelInfo = `⏰ 手动存点 · ${identitySummary}LV.${d.user_level} · 经验 ${d.exp_num} · 金币 ${d.jjb} · 已满级 ✓`;
                } else {
                    levelInfo = `⏰ 手动存点 · ${identitySummary}LV.${d.user_level} · 经验 ${d.exp_num} · 金币 ${d.jjb}`;
                }

                // 本次获得的奖励提示
                let gainInfo = '';
                if (d.exp_gain > 0 || d.jjb_gain > 0) {
                    const parts = [];
                    if (d.exp_gain > 0) parts.push(`经验+${d.exp_gain}`);
                    if (d.jjb_gain > 0) parts.push(`金币+${d.jjb_gain}`);
                    gainInfo = ` 本次获得：${parts.join('，')}`;
                }

                if (data.data.leveled_up) {
                    const upDiv = document.createElement('div');
                    upDiv.className = 'msg-line';
                    upDiv.innerHTML =
                        `<span style="color: #d97706; font-weight: bold;">【系统】恭喜！你的经验值已达 ${d.exp_num}，等级突破至 LV.${d.user_level}！🌟</span><span class="msg-time">(${timeStr})</span>`;
                    container2.appendChild(upDiv);
                    if (autoScroll) container2.scrollTop = container2.scrollHeight;
                }

                if (!silent) {
                    const detailDiv = document.createElement('div');
                    detailDiv.className = 'msg-line';
                    detailDiv.dataset.autosave = '1';
                    detailDiv.innerHTML =
                        `<span style="color: green;">${escapeHtml(levelInfo + gainInfo)}</span><span class="msg-time">(${timeStr})</span>`;
                    // 手动存点和自动存点共用同一条界面占位，避免包厢窗口堆积旧存点通知。
                    lastAutosaveNode?.remove();
                    lastAutosaveNode = detailDiv;
                    container2.appendChild(detailDiv);
                    pruneMessageContainer(container2, PRIVATE_MESSAGE_NODE_LIMIT);
                    if (autoScroll) container2.scrollTop = container2.scrollHeight;
                } else {
                    return;
                }
            }
        } catch (e) {
            console.error('存点失败', e);
            heartbeatFailCount++;

            if (heartbeatFailCount >= MAX_HEARTBEAT_FAILS) {
                window.chatDialog.alert('⚠️ 与服务器的连接已断开，请检查网络后重新登录。', '连接警告', '#b45309');
                window.location.href = '/';
                return;
            }

            if (!silent) {
                const sysDiv = document.createElement('div');
                sysDiv.className = 'msg-line';
                sysDiv.innerHTML = `<span style="color: red;">【系统】存点失败，请稍后重试</span>`;
                container2.appendChild(sysDiv);
            }
        }
    }

    // ── 自动存点心跳（每60秒自动存一次）───────────
    const HEARTBEAT_INTERVAL = 60 * 1000;
    setInterval(() => saveExp(true), HEARTBEAT_INTERVAL);
    setTimeout(() => saveExp(true), 10000);


    /**
     * HTML 转义函数，防止 XSS
     */
    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    /**
     * 规整广播携带的链接，只允许当前站点的 http(s) 地址进入 innerHTML。
     */
    function normalizeSafeChatUrl(url, fallback) {
        try {
            const parsedUrl = new URL(url || fallback, window.location.origin);
            if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
                return fallback;
            }

            if (parsedUrl.origin !== window.location.origin) {
                return fallback;
            }

            return parsedUrl.toString();
        } catch (error) {
            return fallback;
        }
    }
</script>
