Linux.do Assistant

Linux.do 仪表盘 - 信任级别进度 & 积分查看 & CDK社区分数 (支持全等级)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Linux.do Assistant
// @namespace    https://linux.do/
// @version      4.3.0
// @description  Linux.do 仪表盘 - 信任级别进度 & 积分查看 & CDK社区分数 (支持全等级)
// @author       [email protected]
// @match        https://linux.do/*
// @match        https://cdk.linux.do/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_info
// @connect      connect.linux.do
// @connect      credit.linux.do
// @connect      cdk.linux.do
// @connect      raw.githubusercontent.com
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 配置
    const CONFIG = {
        API: {
            TRUST: 'https://connect.linux.do',
            CREDIT_INFO: 'https://credit.linux.do/api/v1/oauth/user-info',
            CREDIT_STATS: 'https://credit.linux.do/api/v1/dashboard/stats/daily?days=7',
            LINK_TRUST: 'https://connect.linux.do/',
            LINK_CREDIT: 'https://credit.linux.do/home',
            LEADERBOARD: 'https://linux.do/leaderboard/1.json',
            LEADERBOARD_DAILY: 'https://linux.do/leaderboard/1.json?period=daily',
            LINK_LEADERBOARD: 'https://linux.do/leaderboard/1',
            CDK_INFO: 'https://cdk.linux.do/api/v1/oauth/user-info',
            LINK_CDK: 'https://cdk.linux.do/dashboard',
            LINK_LOGIN: 'https://linux.do/login',
            // 新增:用于获取用户信息和summary的API
            USER_INFO: (username) => `https://linux.do/u/${username}.json`,
            USER_SUMMARY: (username) => `https://linux.do/u/${username}/summary.json`
        },
        // 0-1级用户升级要求(硬编码)
        LEVEL_REQUIREMENTS: {
            0: { // 0级升1级
                topics_entered: { name: '浏览的话题', target: 5 },
                posts_read_count: { name: '已读帖子', target: 30 },
                time_read: { name: '阅读时间', target: 600, unit: 'seconds' } // 10分钟
            },
            1: { // 1级升2级
                days_visited: { name: '访问天数', target: 15 },
                likes_given: { name: '送出赞', target: 1 },
                likes_received: { name: '获赞', target: 1 },
                post_count: { name: '帖子数量', target: 3 },
                topics_entered: { name: '浏览的话题', target: 20 },
                posts_read_count: { name: '已读帖子', target: 100 },
                time_read: { name: '阅读时间', target: 3600, unit: 'seconds' } // 60分钟
            }
        },
        CACHE_SCHEMA_VERSION: 2,
        // 保持 v4 键名以维持配置
        KEYS: {
            POS: 'lda_v4_pos',
            THEME: 'lda_v4_theme',
            EXPAND: 'lda_v4_expand',
            HEIGHT: 'lda_v4_height',
            LANG: 'lda_v4_lang',
            CACHE_TRUST: 'lda_v4_cache_trust',
            CACHE_TRUST_DATA: 'lda_v5_cache_trust_data',
            CACHE_CREDIT_DATA: 'lda_v5_cache_credit_data',
            TAB_ORDER: 'lda_v5_tab_order',
            CACHE_CDK: 'lda_v5_cache_cdk',
            REFRESH_INTERVAL: 'lda_v5_refresh_interval',
            OPACITY: 'lda_v5_opacity',
            CACHE_META: 'lda_v5_cache_meta',
            CACHE_SCHEMA: 'lda_v5_cache_schema',
            USER_SIG: 'lda_v5_user_sig',
            LAST_SKIP_UPDATE: 'lda_v5_last_skip_update',
            LAST_AUTO_CHECK: 'lda_v5_last_auto_check'
        }
    };
    const AUTO_REFRESH_MS = 30 * 60 * 1000; // 半小时定时刷新

    // 多语言
    const I18N = {
        zh: {
            title: "Linux.do 仪表盘",
            tab_trust: "信任级别",
            tab_credit: "积分详情",
            tab_cdk: "CDK分数",
            tab_setting: "偏好设置",
            loading: "数据加载中...",
            connect_err: "连接失败或未登录",
            trust_not_login: "尚未登录社区",
            trust_login_tip: "登录后可查看信任级别与进度",
            trust_go_login: "前往登录",
            level: "当前级别",
            status_ok: "已达标",
            status_fail: "未达标",
            status_fallback: "降级显示",
            celebrate_title: "🎊 全部达标!",
            celebrate_subtitle: "所有要求均已满足",
            celebrate_msg_upgrade: "享受信任级别 {level} 的所有权限吧!",
            celebrate_msg_lv3: "享受信任级别 3 的所有权限吧!",
            btn_details: "详情",
            btn_collapse: "收起",
            credit_keep_cache_tip: "授权校验异常,已继续显示缓存数据",
            balance: "当前余额",
            daily_limit: "今日剩余额度",
            recent: "近7日收支",
            no_rec: "暂无记录",
            income: "收入",
            expense: "支出",
            set_auto: "自动展开面板",
            set_lang: "界面语言",
            set_size: "面板高度",
            set_opacity: "透明度",
            set_refresh: "自动刷新频率",
            size_sm: "标准",
            size_lg: "加高",
            size_auto: "自适应",
            refresh_30: "30 分钟",
            refresh_60: "1 小时",
            refresh_120: "2 小时",
            refresh_off: "关闭",
            refresh_tip: "仅在面板展开时定时刷新",
            theme_tip: "点击切换:亮色 / 深色 / 跟随系统",
            link_tip: "前往网页版",
            refresh_tip_btn: "刷新数据",
            refresh_done: "刷新完毕",
            check_update: "检查更新",
            checking: "检查中...",
            new_version: "发现新版本",
            latest: "已是最新",
            update_err: "检查失败",
            rank: "总排名",
            rank_today: "今日",
            score: "积分",
            credit_not_auth: "尚未登录 Credit",
            credit_auth_tip: "需先完成授权才能查看积分数据",
            credit_go_auth: "前往登录",
            credit_refresh: "刷新",
            set_tab_order: "标签顺序",
            tab_order_tip: "拖拽调整顺序",
            tab_order_save: "保存顺序",
            tab_order_saved: "已保存",
            cdk_score: "CDK分数",
            cdk_trust_level: "信任等级",
            cdk_username: "用户名",
            cdk_nickname: "昵称",
            cdk_not_auth: "尚未登录 CDK",
            cdk_auth_tip: "需先完成授权才能查看社区分数",
            cdk_go_auth: "前往登录",
            cdk_refresh: "刷新",
            cdk_score_desc: "基于徽章计算的社区信誉分",
            support_title: "支持作者",
            support_desc: "您的支持是持续开发的动力",
            support_thanks: "感谢您的支持 ❤️",
            slow_tip: "请求有点慢,稍等我处理一下…",
            clear_cache: "清除缓存",
            clear_cache_tip: "清除跨标签页缓存与账号关联数据",
            clear_cache_done: "缓存已清空",
            // V3新增:友好错误提示
            network_error_title: "暂时无法获取数据",
            network_error_tip: "可能是网络波动或运行环境问题,请稍后重试",
            network_error_retry: "点击刷新",
            trust_fallback_title: "Connect 数据暂不可用",
            trust_fallback_tip: "未获取到 Connect 完整数据,请稍后刷新再试(当前暂用 Summary 数据展示)",
            trust_data_source: "数据来源",
            // extra hints
            connect_open: "打开 Connect",
            credit_open: "打开 Credit",
            cdk_open: "打开 CDK"
        },
        en: {
            title: "Linux.do HUD",
            tab_trust: "Trust Level",
            tab_credit: "Credits",
            tab_cdk: "CDK Score",
            tab_setting: "Settings",
            loading: "Loading...",
            connect_err: "Connection Error / Not Logged In",
            trust_not_login: "Not logged in",
            trust_login_tip: "Login to view trust level and progress",
            trust_go_login: "Go to Login",
            level: "Level",
            status_ok: "Qualified",
            status_fail: "Unqualified",
            status_fallback: "Fallback",
            celebrate_title: "🎊 All requirements met!",
            celebrate_subtitle: "You have met every requirement",
            celebrate_msg_upgrade: "Enjoy all the privileges of Trust Level {level}!",
            celebrate_msg_lv3: "Enjoy all the privileges of Trust Level 3!",
            btn_details: "Details",
            btn_collapse: "Collapse",
            credit_keep_cache_tip: "Authorization check failed; showing cached data.",
            balance: "Balance",
            daily_limit: "Daily Limit",
            recent: "Recent Activity",
            no_rec: "No activity",
            income: "Income",
            expense: "Expense",
            set_auto: "Auto Expand",
            set_lang: "Language",
            set_size: "Panel Height",
            set_opacity: "Opacity",
            set_refresh: "Auto Refresh",
            size_sm: "Small",
            size_lg: "Tall",
            size_auto: "Auto",
            refresh_30: "30 min",
            refresh_60: "1 hour",
            refresh_120: "2 hours",
            refresh_off: "Off",
            refresh_tip: "Refresh periodically only when panel is open",
            theme_tip: "Toggle: Light / Dark / Auto",
            link_tip: "Open Website",
            refresh_tip_btn: "Refresh",
            refresh_done: "Refreshed",
            check_update: "Check Update",
            checking: "Checking...",
            new_version: "New Version",
            latest: "Up to date",
            update_err: "Check failed",
            rank: "Rank",
            rank_today: "Today",
            score: "Score",
            credit_not_auth: "Credit Not Logged In",
            credit_auth_tip: "Please authorize to view credit data",
            credit_go_auth: "Go to Login",
            credit_refresh: "Refresh",
            set_tab_order: "Tab Order",
            tab_order_tip: "Drag to reorder",
            tab_order_save: "Save Order",
            tab_order_saved: "Saved",
            cdk_score: "CDK Score",
            cdk_trust_level: "Trust Level",
            cdk_username: "Username",
            cdk_nickname: "Nickname",
            cdk_not_auth: "CDK Not Logged In",
            cdk_auth_tip: "Please authorize to view CDK score",
            cdk_go_auth: "Go to Login",
            cdk_refresh: "Refresh",
            cdk_score_desc: "Community reputation based on badges",
            support_title: "Support",
            support_desc: "Your support keeps development going",
            support_thanks: "Thank you for your support ❤️",
            slow_tip: "It's a bit slow, please hold on…",
            clear_cache: "Clear cache",
            clear_cache_tip: "Remove cross-tab cache and user binding",
            clear_cache_done: "Cache cleared",
            // V3 new: friendly error messages
            network_error_title: "Unable to load data",
            network_error_tip: "Network or environment issue, please try again later",
            network_error_retry: "Refresh",
            trust_fallback_title: "Connect unavailable",
            trust_fallback_tip: "Unable to fetch full Connect data. Please refresh later (showing Summary for now).",
            trust_data_source: "Data source",
            connect_open: "Open Connect",
            credit_open: "Open Credit",
            cdk_open: "Open CDK"
        }
    };

    // 工具函数
    class Utils {
        static async request(url, options = {}) {
            const { withCredentials, retries = 3, timeout = 8000, ...validOptions } = options;
            const attempts = Math.max(1, retries);
            let lastErr;
            for (let i = 0; i < attempts; i++) {
                try {
                    const res = await new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url,
                            headers: { 'Cache-Control': 'no-cache' },
                            anonymous: false, // 确保跨域请求发送 cookie
                            timeout,
                            ...validOptions,
                            onload: r => (r.status >= 200 && r.status < 300) ? resolve(r.responseText) : reject(r),
                            onerror: e => reject(e),
                            ontimeout: () => reject(new Error('timeout'))
                        });
                    });
                    return res;
                } catch (e) {
                    if (e?.status === 401 || e?.status === 403) throw e;
                    lastErr = e;
                    if (i === attempts - 1) throw lastErr;
                }
            }
            throw lastErr;
        }
        static get(k, d) { return GM_getValue(k, d); }
        static set(k, v) { GM_setValue(k, v); }
        static html(strings, ...values) { return strings.reduce((r, s, i) => r + s + (values[i] || ''), ''); }
        static el(s, p = document) { return p.querySelector(s); }
        static els(s, p = document) { return p.querySelectorAll(s); }

        // 获取当前登录用户名(保留旧逻辑,作为兜底)
        static getCurrentUsername() {
            // 方法1: 从 Discourse 全局对象获取
            try {
                const currentUser = window.Discourse?.User?.current?.() ||
                    window.Discourse?.currentUser ||
                    window.User?.current?.();
                if (currentUser?.username) return currentUser.username;
            } catch (e) { }

            // 方法2: 从页面 meta 标签或 preload 数据获取
            try {
                const preloadData = document.getElementById('data-preloaded');
                if (preloadData) {
                    const data = JSON.parse(preloadData.dataset.preloaded);
                    if (data?.currentUser) {
                        const cu = JSON.parse(data.currentUser);
                        if (cu?.username) return cu.username;
                    }
                }
            } catch (e) { }

            // 方法3: 从导航栏用户头像链接获取
            try {
                const avatarLink = document.querySelector('#current-user a[href*="/u/"]');
                if (avatarLink) {
                    const match = avatarLink.href.match(/\/u\/([^\/]+)/);
                    if (match) return match[1];
                }
            } catch (e) { }

            // 方法4: 从 localStorage 获取(Discourse 常用存储)
            try {
                const stored = localStorage.getItem('discourse_current_user');
                if (stored) {
                    const parsed = JSON.parse(stored);
                    if (parsed?.username) return parsed.username;
                }
            } catch (e) { }

            return null;
        }

        // ✅ 新增:权威 session 登录判定(同源)
        static async fetchSessionUser() {
            try {
                const r = await fetch('/session/current.json', { credentials: 'include' });
                if (!r.ok) return null;
                const data = await r.json();
                return data?.current_user || null;
            } catch (_) {
                return null;
            }
        }
        // v4-inspired: DOM-based login & username detection (used for cache/user-switch and as fallback)
        // Return: true (logged-in) / false (guest) / null (unknown)
        static getLoginStateByDOM() {
            try {
                const header = document.querySelector('.d-header') || document;
                const hasUser = !!header.querySelector('.header-dropdown-toggle.current-user, a.current-user, .current-user');
                if (hasUser) return true;

                const els = Array.from(header.querySelectorAll('a[href], button, .btn'));
                const hasLogin = els.some(el => {
                    const href = (el.getAttribute('href') || '').toLowerCase();
                    const text = (el.textContent || '').trim().toLowerCase();
                    // 只在 header 范围内检测“登录/注册”入口(借鉴 v4:无登录/注册按钮通常意味着已登录)
                    return href.includes('/login') || href.includes('/session')
                        || href.includes('/signup') || href.includes('/register')
                        || /登录|注册|log\s*in|sign\s*in|sign\s*up|register/.test(text);
                });

                if (hasLogin) return false;
                return null;
            } catch (_) {
                return null;
            }
        }

        // 尝试多种方式获取当前用户名(参考 v4 的 getCurrentUsername)
        static getCurrentUsernameFromDOM() {
            try {
                // 方法1:用户菜单头像 alt
                const userMenuButton = document.querySelector('.header-dropdown-toggle.current-user');
                if (userMenuButton) {
                    const img = userMenuButton.querySelector('img');
                    const alt = (img?.alt || '').trim();
                    if (alt) return alt.replace(/^@/, '');
                }

                // 方法2:用户头像 title
                const userAvatar = document.querySelector('.current-user img[title]');
                if (userAvatar && userAvatar.title) return userAvatar.title.trim().replace(/^@/, '');

                // 方法3:当前用户链接
                const currentUserLink = document.querySelector('a.current-user, .header-dropdown-toggle.current-user a');
                if (currentUserLink) {
                    const href = currentUserLink.getAttribute('href');
                    if (href && href.includes('/u/')) {
                        const username = href.split('/u/')[1].split('/')[0];
                        if (username) return username.trim().replace(/^@/, '');
                    }
                }

                // 方法4:遍历页面用户链接(排除 topic 列表 / 帖子流)
                const userLinks = document.querySelectorAll('a[href*="/u/"]');
                for (const link of userLinks) {
                    if (link.closest('.topic-list') || link.closest('.post-stream')) continue;
                    const href = link.getAttribute('href');
                    if (href && href.includes('/u/')) {
                        const username = href.split('/u/')[1].split('/')[0];
                        if (username) return username.trim().replace(/^@/, '');
                    }
                }

                // 方法5:URL 在用户页
                if (window.location.pathname.includes('/u/')) {
                    const username = window.location.pathname.split('/u/')[1].split('/')[0];
                    if (username) return username.trim().replace(/^@/, '');
                }

                // 方法6:localStorage(Discourse 当前用户)
                try {
                    const discourseData = localStorage.getItem('discourse_current_user');
                    if (discourseData) {
                        const userData = JSON.parse(discourseData);
                        if (userData?.username) return String(userData.username).trim().replace(/^@/, '');
                    }
                } catch (_) { /* ignore */ }

                return null;
            } catch (_) {
                return null;
            }
        }

        // 从 connect.linux.do 的欢迎语中解析“用户名 + 当前等级”(参考 v4 逻辑)
        static async fetchConnectWelcome() {
            const html = await Utils.request(CONFIG.API.TRUST, { timeout: 15000, retries: 2 });
            const doc = new DOMParser().parseFromString(html, 'text/html');
            const bodyText = doc.body?.textContent || '';
            const loginHint = doc.querySelector('a[href*="/login"], form[action*="/login"], form[action*="/session"]');
            if (loginHint || /登录|login|sign\s*in/i.test(bodyText)) {
                const err = new Error('NeedLogin');
                err.code = 'NeedLogin';
                throw err;
            }

            const h1 = doc.querySelector('h1');
            const h1Text = (h1?.textContent || '').trim();

            let username = null;
            let level = null;

            // 例如: "你好,一剑万生 (YY_WD) 2级用户"
            let m = h1Text.match(/你好,\s*([^\(\s]*)\s*\(?([^)]*)\)?\s*(\d+)\s*级用户/i);
            if (m) {
                username = (m[2] || m[1] || '').trim();
                level = (m[3] || '').trim();
            }

            // 英文兜底(不严格)
            if (!level) {
                const m2 = h1Text.match(/trust\s*level\s*(\d+)/i) || h1Text.match(/(\d+)\s*(?:level|lvl)/i);
                if (m2) level = (m2[1] || '').trim();
            }

            if (username) username = username.replace(/^@/, '');
            const trustLevel = level !== null && level !== '' && !Number.isNaN(Number(level)) ? Number(level) : null;

            if (!username && trustLevel === null) return null;
            return { username, trustLevel };
        }


        // 获取用户信息(含信任等级)- 使用同源请求更稳定
        static async fetchUserInfo(username) {
            if (!username) return null;
            try {
                const r = await fetch(CONFIG.API.USER_INFO(username), { credentials: 'include' });
                if (!r.ok) return null;
                const data = await r.json();
                return data?.user || null;
            } catch (e) {
                return null;
            }
        }

        // 获取用户 summary 数据
        static async fetchUserSummary(username) {
            if (!username) return null;
            try {
                const r = await fetch(CONFIG.API.USER_SUMMARY(username), { credentials: 'include' });
                if (!r.ok) return null;
                const data = await r.json();
                return data?.user_summary || null;
            } catch (e) {
                return null;
            }
        }

        // 格式化阅读时间(秒 -> 可读格式)
        static formatReadTime(seconds) {
            const s = Number(seconds) || 0;
            if (s < 60) return `${s}秒`;
            const minutes = Math.floor(s / 60);
            if (minutes < 60) return `${minutes}分钟`;
            const hours = Math.floor(minutes / 60);
            const remainMins = minutes % 60;
            return remainMins > 0 ? `${hours}小时${remainMins}分` : `${hours}小时`;
        }

        // 获取论坛排名数据
        static async fetchForumStats() {
            const baseUrl = window.location.origin;
            const fetchJson = async (url) => {
                let lastErr = null;
                for (let i = 0; i < 3; i++) {
                    const controller = new AbortController();
                    const timer = setTimeout(() => controller.abort(), 10000);
                    try {
                        const r = await fetch(url, { signal: controller.signal });
                        clearTimeout(timer);
                        if (!r.ok) throw new Error(`http ${r.status}`);
                        return await r.json();
                    } catch (e) {
                        clearTimeout(timer);
                        lastErr = e;
                        if (i === 2) return null;
                    }
                }
                return null;
            };
            try {
                const [daily, global] = await Promise.all([
                    fetchJson(CONFIG.API.LEADERBOARD_DAILY),
                    fetchJson(CONFIG.API.LEADERBOARD)
                ]);
                // 尝试从 leaderboard 获取积分
                let score = global?.personal?.total_score || global?.personal?.score || null;
                // 如果没有积分,尝试从用户 API 获取
                if (!score && global?.personal?.user?.username) {
                    const username = global.personal.user.username;
                    const userInfo = await fetchJson(`${baseUrl}/u/${username}.json`);
                    score = userInfo?.user?.gamification_score || null;
                }
                return {
                    dailyRank: daily?.personal?.position || null,
                    globalRank: global?.personal?.position || null,
                    score: score
                };
            } catch (e) {
                return { dailyRank: null, globalRank: null, score: null };
            }
        }
    }

    // --- CDK Bridge (Tampermonkey 兼容兜底) ---
    const CDK_BRIDGE_ORIGIN = 'https://cdk.linux.do';
    const CDK_CACHE_TTL = 5 * 60 * 1000;
    const isCDKPage = location.hostname === 'cdk.linux.do';

    // 在 CDK 域内只做数据桥接,不渲染面板
    const initCDKBridgePage = () => {
        const cacheAndNotify = async () => {
            try {
                const res = await fetch(CONFIG.API.CDK_INFO, { credentials: 'include' });
                if (!res.ok) return;
                const json = await res.json();
                if (!json?.data) return;
                Utils.set(CONFIG.KEYS.CACHE_CDK, { data: json.data, ts: Date.now() });
                try {
                    window.parent?.postMessage({ type: 'lda-cdk-data', payload: { data: json.data } }, '*');
                } catch (_) { }
            } catch (_) { }
        };

        // 初始化立即拉取一次
        cacheAndNotify();

        // 接收来自 linux.do 的请求再拉取一次
        window.addEventListener('message', (e) => {
            if (e.data?.type === 'lda-cdk-request') cacheAndNotify();
        });
    };

    if (isCDKPage) {
        initCDKBridgePage();
        return;
    }

    // 样式
    const Styles = `
        :root {
            --lda-bg: rgba(255, 255, 255, 0.94);
            --lda-fg: #0f172a;
            --lda-dim: #64748b;
            --lda-border: 1px solid rgba(0,0,0,0.08);
            --lda-shadow: 0 12px 30px -4px rgba(0, 0, 0, 0.12);
            --lda-accent: #3b82f6;
            --lda-ball-ring: rgba(0,0,0,0.08);
            --lda-rad: 12px;
            --lda-z: 99999;
            --lda-opacity: 1;
            --lda-ball-size: 40px;
            --lda-ball-radius: 14px;
            --lda-red: #ef4444;
            --lda-green: #22c55e;
            --lda-neutral: rgba(125,125,125,0.25);
            --lda-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        }
        .lda-dark {
            --lda-bg: rgba(15, 23, 42, 0.94);
            --lda-fg: #f1f5f9;
            --lda-dim: #94a3b8;
            --lda-border: 1px solid rgba(255,255,255,0.08);
            --lda-shadow: 0 12px 30px -4px rgba(0, 0, 0, 0.6);
            --lda-accent: #38bdf8;
            --lda-ball-ring: rgba(255,255,255,0.15);
            --lda-neutral: rgba(255,255,255,0.18);
        }

        #lda-root { position: fixed; z-index: var(--lda-z); font-family: var(--lda-font); font-size: 14px; user-select: none; color: var(--lda-fg); min-width: var(--lda-ball-size); min-height: var(--lda-ball-size); opacity: var(--lda-opacity); transition: opacity 0.2s ease; }

        /* 悬浮球 */
        .lda-ball {
            position: relative;
            width: var(--lda-ball-size); height: var(--lda-ball-size);
            background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
            border-radius: var(--lda-ball-radius);
            box-shadow: 0 8px 22px rgba(59, 130, 246, 0.35), 0 0 0 1px var(--lda-ball-ring);
            border: none;
            cursor: grab;
            display: flex; align-items: center; justify-content: center; color: #fff;
            transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s;
            overflow: hidden;
        }
        .lda-ball::after {
            content: "";
            position: absolute;
            inset: 2px;
            border-radius: inherit;
            background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.25), transparent 55%);
            pointer-events: none;
            opacity: 0.9;
        }
        .lda-ball:hover { transform: scale(1.08) rotate(6deg); box-shadow: 0 10px 26px rgba(59, 130, 246, 0.45); }
        .lda-ball.dragging { cursor: grabbing; transform: scale(1.12); box-shadow: 0 12px 28px rgba(59, 130, 246, 0.55); }
        .lda-ball svg { width: 20px; height: 20px; fill: currentColor; pointer-events: none; position: relative; z-index: 1; }

        /* 面板 */
        .lda-panel {
            position: absolute; top: 0; right: 0;
            width: clamp(300px, calc(100vw - 24px), 370px); background: var(--lda-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
            border: var(--lda-border); border-radius: var(--lda-rad); box-shadow: var(--lda-shadow);
            display: none; flex-direction: column; overflow: hidden; margin-top: 0;
            transform-origin: top right; animation: lda-in 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
        }
        #lda-root.lda-side-right .lda-panel { left: 0; right: auto; transform-origin: top left; }
        #lda-root.lda-side-left .lda-panel { right: 0; left: auto; transform-origin: top right; }
        @keyframes lda-in { from { opacity: 0; transform: scale(0.92) translateY(-10px); } to { opacity: 1; transform: scale(1) translateY(0); } }

        /* 头部 */
        .lda-head {
            padding: 12px 16px; display: flex; justify-content: space-between; align-items: center;
            border-bottom: var(--lda-border); background: rgba(125,125,125,0.03); cursor: move;
        }
        .lda-title { font-weight: 700; font-size: 13px; color: var(--lda-accent); letter-spacing: -0.3px; }
        .lda-actions { display: flex; gap: 8px; }
        .lda-icon-btn {
            width: 24px; height: 24px; border-radius: 6px; display: flex; align-items: center; justify-content: center;
            cursor: pointer; opacity: 0.6; transition: 0.2s; color: var(--lda-fg);
        }
        .lda-icon-btn:hover { background: rgba(125,125,125,0.1); opacity: 1; }

        /* 导航 */
        .lda-tabs { display: flex; padding: 6px 16px 0; border-bottom: var(--lda-border); gap: 16px; }
        .lda-tab {
            padding: 8px 0; font-size: 12px; cursor: pointer; color: var(--lda-dim);
            border-bottom: 2px solid transparent; transition: 0.2s; font-weight: 500;
        }
        .lda-tab:hover { color: var(--lda-fg); }
        .lda-tab.active { border-bottom-color: var(--lda-accent); color: var(--lda-accent); font-weight: 600; }

        /* 内容区 */
        .lda-body { position: relative; transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
        .lda-page { display: none; padding: 16px; animation: lda-fade 0.2s; }
        .lda-page.active { display: block; }
        @keyframes lda-fade { from { opacity: 0; transform: translateX(6px); } to { opacity: 1; transform: translateX(0); } }

        /* 高度控制 */
        .h-sm .lda-body { height: 320px; overflow-y: auto; }
        .h-lg .lda-body { height: 520px; overflow-y: auto; }
        .h-auto .lda-body { height: auto; max-height: 80vh; min-height: 200px; overflow-y: auto; }
        .lda-body::-webkit-scrollbar { width: 4px; }

        /* 移动端响应式适配 */
        @media (max-width: 420px) {
            .h-auto .lda-body { max-height: 65vh; }
        }
        .lda-body::-webkit-scrollbar-thumb { background: rgba(125,125,125,0.2); border-radius: 2px; }

        /* 卡片通用 */
        .lda-card {
            background: rgba(125,125,125,0.03); border-radius: 10px; padding: 14px; margin-bottom: 12px;
            border: var(--lda-border); position: relative;
        }

        /* 头部信息栏 & 动作按钮组 */
        .lda-info-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; min-height: 28px; }
        .lda-lvl-group { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; padding-right: 60px; /* 留出右侧按钮空间 */ }
        .lda-big-lvl { font-size: 20px; font-weight: 800; color: var(--lda-accent); line-height: 1; }
        .lda-badge {
            padding: 3px 8px; border-radius: 6px; font-size: 11px; font-weight: 600;
            background: rgba(125,125,125,0.1); display: inline-block;
        }
        .lda-badge.ok { background: rgba(34, 197, 94, 0.1); color: var(--lda-green); }
        .lda-badge.no { background: rgba(239, 68, 68, 0.1); color: var(--lda-red); }
        .lda-badge.neutral { background: rgba(125,125,125,0.10); color: var(--lda-dim); }

        /* 排名统计栏 */
        .lda-stats-bar {
            display: flex; gap: 10px; margin-top: 10px; padding: 8px 10px;
            background: rgba(125,125,125,0.05); border-radius: 8px; white-space: nowrap;
        }
        .lda-stats-bar a { text-decoration: none; color: inherit; }
        .lda-stat-item { display: flex; align-items: center; gap: 3px; font-size: 11px; color: var(--lda-dim); }
        .lda-stat-item .num { font-weight: 700; font-size: 13px; }
        .lda-stat-item .num.rank { color: #e74c3c; }
        .lda-stat-item .num.today { color: #f39c12; }
        .lda-stat-item .num.score { color: #27ae60; }

        /* 动作组容器 */
        .lda-actions-group {
            position: absolute; top: 12px; right: 12px;
            display: flex; gap: 6px;
        }

        /* 统一的动作按钮样式 */
        .lda-act-btn {
            width: 28px; height: 28px; border-radius: 50%;
            display: flex; align-items: center; justify-content: center;
            cursor: pointer; background: var(--lda-bg); box-shadow: 0 2px 6px rgba(0,0,0,0.06);
            color: var(--lda-dim); transition: 0.2s; border: var(--lda-border);
            text-decoration: none; /* 针对 a 标签 */
        }
        .lda-act-btn:hover { color: var(--lda-accent); background: #fff; }
        .lda-dark .lda-act-btn:hover { background: rgba(255,255,255,0.1); }

        /* 刷新按钮旋转逻辑 */
        .lda-act-btn.loading svg { animation: lda-spin 0.8s linear infinite; }

        /* 信任列表条目 */
        .lda-item { margin-bottom: 10px; }
        .lda-item-top { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px; }
        .lda-i-name { color: var(--lda-dim); }
        .lda-i-val { font-family: 'SF Mono', monospace; font-weight: 600; display: flex; align-items: center; }
        .lda-progress { height: 5px; background: rgba(125,125,125,0.1); border-radius: 3px; overflow: hidden; }
        .lda-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease-out; }

        /* 涨跌 Diff */
        .lda-diff {
            font-size: 10px; padding: 1px 4px; border-radius: 4px; font-weight: 700; margin-left: 6px;
            display: inline-flex; align-items: center; height: 16px;
        }
        .lda-diff.up { color: var(--lda-red); background: rgba(239, 68, 68, 0.1); }
        .lda-diff.down { color: var(--lda-green); background: rgba(34, 197, 94, 0.1); }

        /* 积分 */
        .lda-credit-hero { text-align: center; padding: 20px 0; }
        .lda-credit-num { font-size: 28px; font-weight: 700; color: var(--lda-fg); font-family: monospace; letter-spacing: -1px; }
        .lda-credit-label { font-size: 11px; text-transform: uppercase; color: var(--lda-dim); margin-top: 4px; letter-spacing: 1px; }

        .lda-row-rec { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px dashed rgba(125,125,125,0.2); font-size: 12px; }

        /* Credit 授权提示卡片 */
        .lda-auth-card { text-align: center; padding: 30px 20px; }
        .lda-auth-icon { color: var(--lda-dim); opacity: 0.5; margin-bottom: 12px; }
        .lda-auth-title { font-size: 15px; font-weight: 600; color: var(--lda-fg); margin-bottom: 6px; }
        .lda-auth-tip { font-size: 12px; color: var(--lda-dim); margin-bottom: 16px; }
        .lda-auth-btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
        .lda-auth-btn {
            display: inline-block; padding: 10px 20px; background: var(--lda-accent); color: #fff;
            border-radius: 8px; font-size: 13px; font-weight: 600; text-decoration: none;
            transition: all 0.2s; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); border: none; cursor: pointer;
        }
        .lda-auth-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); }
        .lda-auth-btn.secondary { background: rgba(125,125,125,0.15); color: var(--lda-fg); box-shadow: none; }
        .lda-auth-btn.secondary:hover { background: rgba(125,125,125,0.25); transform: none; }
        .lda-row-rec:last-child { border: none; }
        .lda-amt { font-weight: 600; font-family: monospace; }

        /* 设置 */
        .lda-opt { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; padding-bottom: 14px; border-bottom: var(--lda-border); }
        .lda-opt:last-child { border: none; margin: 0; padding: 0; }
        .lda-opt-label { font-size: 13px; font-weight: 500; }
        .lda-opt-right { display: flex; flex-direction: column; gap: 8px; align-items: flex-end; }
        .lda-opt-sub { font-size: 12px; color: var(--lda-dim); }

        .lda-switch { position: relative; width: 36px; height: 20px; display: inline-block; }
        .lda-switch input { opacity: 0; width: 0; height: 0; }
        .lda-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #cbd5e1; transition: .3s; border-radius: 20px; }
        .lda-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
        input:checked + .lda-slider { background-color: var(--lda-accent); }
        input:checked + .lda-slider:before { transform: translateX(16px); }

        .lda-seg { display: flex; background: rgba(125,125,125,0.08); padding: 3px; border-radius: 8px; }
        .lda-seg-item { padding: 4px 10px; font-size: 11px; cursor: pointer; border-radius: 6px; color: var(--lda-dim); font-weight: 500; transition: 0.2s; }
        .lda-seg-item.active { background: var(--lda-bg); color: var(--lda-fg); box-shadow: 0 2px 5px rgba(0,0,0,0.05); font-weight: 600; }

        .lda-opacity-row { display: flex; align-items: center; gap: 8px; }
        .lda-range {
            -webkit-appearance: none;
            appearance: none;
            width: 140px;
            height: 6px;
            border-radius: 999px;
            background: linear-gradient(90deg, rgba(59,130,246,0.15), rgba(59,130,246,0.35));
            outline: none;
            cursor: pointer;
        }
        .lda-range::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: var(--lda-accent);
            box-shadow: 0 2px 6px rgba(0,0,0,0.2);
            border: 1px solid rgba(255,255,255,0.6);
        }
        .lda-range::-moz-range-thumb {
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: var(--lda-accent);
            box-shadow: 0 2px 6px rgba(0,0,0,0.2);
            border: 1px solid rgba(255,255,255,0.6);
        }

        .lda-spin { animation: lda-spin 0.8s linear infinite; }
        @keyframes lda-spin { 100% { transform: rotate(360deg); } }

        /* 云朵脉冲动画(检查更新) */
        .lda-cloud-pulse { animation: lda-cloud-pulse 0.6s ease-in-out infinite; }
        @keyframes lda-cloud-pulse {
            0%, 100% { opacity: 0.5; transform: scale(1); }
            50% { opacity: 1; transform: scale(1.2); }
        }

        /* 拖拽排序 */
        .lda-sortable { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
        .lda-sort-item {
            display: flex; align-items: center; gap: 10px; padding: 10px 12px;
            background: var(--lda-bg); border: var(--lda-border); border-radius: 8px;
            cursor: grab; user-select: none; transition: all 0.2s;
        }
        .lda-sort-item:hover { background: rgba(125,125,125,0.08); }
        .lda-sort-item.dragging { opacity: 0.5; cursor: grabbing; transform: scale(1.02); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
        .lda-sort-item.drag-over { border-color: var(--lda-accent); background: rgba(59, 130, 246, 0.08); }
        .lda-sort-handle { color: var(--lda-dim); display: flex; align-items: center; }
        .lda-sort-label { flex: 1; font-size: 13px; font-weight: 500; }
        .lda-sort-num { width: 20px; height: 20px; border-radius: 50%; background: var(--lda-accent); color: #fff; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
        .lda-sort-btn {
            margin-top: 8px; padding: 8px 16px; background: var(--lda-accent); color: #fff;
            border: none; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer;
            transition: all 0.2s; width: 100%;
        }
        .lda-sort-btn:hover { opacity: 0.9; }
        .lda-sort-btn.saved { background: var(--lda-green); }

        /* 支持作者区域 */
        .lda-support {
            background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(249, 115, 22, 0.06), rgba(59, 130, 246, 0.08));
            border-radius: 10px; padding: 10px 12px; margin-bottom: 10px;
            border: 1px solid rgba(239, 68, 68, 0.15);
        }
        .lda-support-header {
            display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;
        }
        .lda-support-title {
            font-size: 13px; font-weight: 600; color: var(--lda-fg);
            display: flex; align-items: center; gap: 6px;
        }
        .lda-support-heart {
            display: inline-block; animation: lda-heartbeat 1.2s ease-in-out infinite;
            filter: drop-shadow(0 0 3px rgba(239, 68, 68, 0.4));
        }
        @keyframes lda-heartbeat {
            0%, 100% { transform: scale(1); }
            14% { transform: scale(1.15); }
            28% { transform: scale(1); }
            42% { transform: scale(1.1); }
            70% { transform: scale(1); }
        }
        .lda-support-desc { font-size: 10px; color: var(--lda-dim); }
        .lda-support-grid {
            display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
        }
        .lda-support-card {
            display: flex; flex-direction: column; align-items: center; padding: 8px 6px;
            background: var(--lda-bg); border: 1px solid var(--lda-border); border-radius: 8px;
            cursor: pointer; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
            text-decoration: none !important; position: relative; overflow: hidden;
        }
        .lda-support-card::before {
            content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
            background: var(--card-accent, var(--lda-accent)); transition: height 0.25s;
        }
        .lda-support-card:hover {
            border-color: var(--card-accent, var(--lda-accent));
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        }
        .lda-support-card:hover::before { height: 3px; }
        .lda-support-card.tier-1 { --card-accent: #10b981; }
        .lda-support-card.tier-2 { --card-accent: #3b82f6; }
        .lda-support-card.tier-3 { --card-accent: #f59e0b; }
        .lda-support-card.tier-4 { --card-accent: #ef4444; }
        .lda-support-icon { font-size: 16px; margin-bottom: 2px; }
        .lda-support-amount {
            font-size: 12px; font-weight: 700; color: var(--card-accent, var(--lda-accent));
        }
        .lda-support-unit { font-size: 9px; color: var(--lda-dim); margin-top: 1px; }
        /* 慢速提示 */
        .lda-slow-tip {
            display: none;
            margin-top: 12px;
            padding: 10px 12px;
            background: rgba(59,130,246,0.08);
            border: 1px dashed rgba(59,130,246,0.4);
            color: var(--lda-dim);
            font-size: 12px;
            border-radius: 8px;
        }

        /* V3新增:降级提示横幅 */
        .lda-fallback-banner {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 10px 12px;
            margin-bottom: 12px;
            background: rgba(251, 191, 36, 0.1);
            border: 1px solid rgba(251, 191, 36, 0.25);
            border-radius: 8px;
            font-size: 11px;
            color: var(--lda-dim);
        }
        .lda-dark .lda-fallback-banner {
            background: rgba(251, 191, 36, 0.08);
            border-color: rgba(251, 191, 36, 0.2);
        }
        .lda-fallback-banner svg {
            flex-shrink: 0;
            width: 16px;
            height: 16px;
            color: #f59e0b;
        }
        .lda-fallback-text {
            flex: 1;
            line-height: 1.4;
        }

        /* V3新增:数据来源标签 */
        .lda-source-tag {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            padding: 2px 8px;
            background: rgba(125,125,125,0.08);
            border-radius: 4px;
            font-size: 10px;
            color: var(--lda-dim);
            margin-left: 8px;
        }
    
        /* === Celebration (all requirements met) === */
        @keyframes lda-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
        .lda-celebration-wrap { display: flex; flex-direction: column; gap: 12px; }
        .lda-celebration-achievement {
            display: flex; flex-direction: column; align-items: center; text-align: center;
            padding: 16px 12px;
            border: var(--lda-border);
            border-radius: var(--lda-rad);
            background: linear-gradient(135deg, rgba(59,130,246,0.12), rgba(34,197,94,0.10));
            position: relative;
            overflow: hidden;
        }
        .lda-celebration-icon {
            position: relative;
            width: 56px; height: 56px;
            border-radius: 18px;
            display: flex; align-items: center; justify-content: center;
            background: var(--lda-accent);
            box-shadow: 0 12px 30px -10px rgba(0,0,0,0.25);
        }
        .lda-celebration-ring {
            position: absolute;
            inset: -10px;
            border-radius: 999px;
            border: 2px solid rgba(255,255,255,0.35);
            animation: lda-spin 2.2s linear infinite;
        }
        .lda-celebration-ring-outer {
            position: absolute;
            inset: -18px;
            border-radius: 999px;
            border: 2px solid rgba(255,255,255,0.18);
            animation: lda-spin 3.8s linear infinite reverse;
        }
        .lda-celebration-title { font-weight: 900; font-size: 16px; margin-top: 10px; color: var(--lda-fg); }
        .lda-celebration-subtitle { font-size: 12px; color: var(--lda-dim); margin-top: 4px; }
        .lda-celebration-message { font-size: 12px; color: var(--lda-fg); margin-top: 10px; line-height: 1.5; }
        .lda-celebration-actions { display: flex; justify-content: center; }
        .lda-celebration-actions button { min-width: 88px; }
        .lda-celebration-details { display: none; flex-direction: column; gap: 10px; }
        .lda-celebration-scroll { max-height: 300px; overflow-y: auto; padding-right: 6px; }

    `;

    // 主程序
    class App {
        constructor() {
            this.state = {
                lang: Utils.get(CONFIG.KEYS.LANG, 'zh'),
                theme: Utils.get(CONFIG.KEYS.THEME, 'auto'),
                height: Utils.get(CONFIG.KEYS.HEIGHT, 'auto'), // Default: Auto
                expand: Utils.get(CONFIG.KEYS.EXPAND, true),   // Default: True
                trustCache: Utils.get(CONFIG.KEYS.CACHE_TRUST, {}),
                tabOrder: Utils.get(CONFIG.KEYS.TAB_ORDER, ['trust', 'credit', 'cdk']), // 标签顺序
                refreshInterval: Utils.get(CONFIG.KEYS.REFRESH_INTERVAL, 30), // 分钟,0 为关闭
                opacity: Utils.get(CONFIG.KEYS.OPACITY, 1)
            };
            this.cdkCache = Utils.get(CONFIG.KEYS.CACHE_CDK, null);
            this.trustData = Utils.get(CONFIG.KEYS.CACHE_TRUST_DATA, null);
            this.creditData = Utils.get(CONFIG.KEYS.CACHE_CREDIT_DATA, null);
            this.lastFetch = Utils.get(CONFIG.KEYS.CACHE_META, { trust: 0, credit: 0, cdk: 0 });
            this.userSig = Utils.get(CONFIG.KEYS.USER_SIG, null);
            this.lastSkipUpdate = Utils.get(CONFIG.KEYS.LAST_SKIP_UPDATE, 0);
            this.lastAutoCheck = Utils.get(CONFIG.KEYS.LAST_AUTO_CHECK, 0);
            this.focusFlags = { trust: false, credit: false, cdk: false };
            this.autoRefreshTimer = null;
            this.userWatchTimer = null; // 账号切换/退出检测计时器
            this.cdkBridgeInit = false;
            this.cdkBridgeFrame = null;
            this.cdkWaiters = [];
            this.onCDKMessage = this.onCDKMessage.bind(this);
            this.activePage = 'trust';
            this.pendingStatus = {
                trust: { count: 0, since: null, timer: null, slowShown: false },
                credit: { count: 0, since: null, timer: null, slowShown: false },
                cdk: { count: 0, since: null, timer: null, slowShown: false }
            };
            // 新增:追踪各页面的刷新状态(用于按钮旋转动画)
            this.refreshingPages = { trust: false, credit: false, cdk: false };
            this.refreshStartTime = { trust: 0, credit: 0, cdk: 0 };
            this.refreshStopPending = { trust: false, credit: false, cdk: false }; // 是否正在等待延迟停止
            this.dom = {};

            // 存储/缓存格式校验(避免旧版本残留导致错误状态)
            this.ensureStorageSchema();
            this.validateLoadedCache();
        }

        async init(forceOpen = false) {
            if (this.autoRefreshTimer) {
                clearInterval(this.autoRefreshTimer);
                this.autoRefreshTimer = null;
            }
            GM_addStyle(Styles);
            this.renderLayout();
            this.bindGlobalEvents();
            this.startUserWatcher();
            this.applyTheme();
            this.applyHeight();
            this.applyOpacity();
            this.restorePos();
            this.updatePanelSide();
            this.renderFromCacheAll();
            this.prewarmAll();
            this.startAutoRefreshTimer();
            this.maybeAutoCheckUpdate();

            if (this.state.expand || forceOpen) {
                this.togglePanel(true);
            }
        }

        t(key) { return I18N[this.state.lang][key] || key; }

        renderLayout() {
            const root = document.createElement('div');
            root.id = 'lda-root';
            root.className = 'lda-side-left';
            // 定义所有标签的映射
            const tabMap = {
                trust: { key: 'trust', label: this.t('tab_trust') },
                credit: { key: 'credit', label: this.t('tab_credit') },
                cdk: { key: 'cdk', label: this.t('tab_cdk') }
            };
            // 根据 tabOrder 获取排序后的标签
            const orderedTabs = this.state.tabOrder.map(key => tabMap[key]);
            root.innerHTML = Utils.html`
                <div class="lda-ball" title="${this.t('title')}">
                    <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
                </div>
                <div class="lda-panel">
                    <div class="lda-head">
                        <div class="lda-title">Linux.do 小秘书</div>
                        <div class="lda-actions">
                            <div class="lda-icon-btn" id="lda-btn-update" title="${this.t('check_update')}"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/></svg></div>
                            <div class="lda-icon-btn" id="lda-btn-theme" title="${this.t('theme_tip')}"></div>
                            <div class="lda-icon-btn" id="lda-btn-close"><svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></div>
                        </div>
                    </div>
                    <div class="lda-tabs">
                        <div class="lda-tab active" data-target="${orderedTabs[0].key}">${orderedTabs[0].label}</div>
                        <div class="lda-tab" data-target="${orderedTabs[1].key}">${orderedTabs[1].label}</div>
                        <div class="lda-tab" data-target="${orderedTabs[2].key}">${orderedTabs[2].label}</div>
                        <div class="lda-tab" data-target="setting">${this.t('tab_setting')}</div>
                    </div>
                    <div class="lda-body">
                        <div id="page-${orderedTabs[0].key}" class="lda-page active">
                            <div id="content-${orderedTabs[0].key}"></div>
                            <div class="lda-slow-tip" data-page="${orderedTabs[0].key}"></div>
                        </div>
                        <div id="page-${orderedTabs[1].key}" class="lda-page">
                            <div id="content-${orderedTabs[1].key}"></div>
                            <div class="lda-slow-tip" data-page="${orderedTabs[1].key}"></div>
                        </div>
                        <div id="page-${orderedTabs[2].key}" class="lda-page">
                            <div id="content-${orderedTabs[2].key}"></div>
                            <div class="lda-slow-tip" data-page="${orderedTabs[2].key}"></div>
                        </div>
                        <div id="page-setting" class="lda-page">
                            <div id="content-setting"></div>
                        </div>
                    </div>
                </div>
            `;
            document.body.appendChild(root);

            this.dom = {
                root,
                ball: Utils.el('.lda-ball', root),
                panel: Utils.el('.lda-panel', root),
                trustPage: Utils.el('#page-trust', root),
                creditPage: Utils.el('#page-credit', root),
                cdkPage: Utils.el('#page-cdk', root),
                settingPage: Utils.el('#page-setting', root),
                trust: Utils.el('#content-trust', root),
                credit: Utils.el('#content-credit', root),
                cdk: Utils.el('#content-cdk', root),
                setting: Utils.el('#content-setting', root),
                slowTips: Utils.els('.lda-slow-tip', root),
                themeBtn: Utils.el('#lda-btn-theme', root),
                tabs: Utils.els('.lda-tab', root),
                head: Utils.el('.lda-head', root)
            };

            this.renderSettings();
            this.updateThemeIcon();
        }

        renderSettings() {
            const { lang, height, expand, tabOrder, refreshInterval, opacity } = this.state;
            const r = (val, cur) => val === cur ? 'active' : '';
            const opacityVal = Math.max(0.5, Math.min(1, Number(opacity) || 1));
            const opacityPercent = Math.round(opacityVal * 100);

            // 标签名称映射
            const tabNames = {
                trust: this.t('tab_trust'),
                credit: this.t('tab_credit'),
                cdk: this.t('tab_cdk')
            };

            // 生成排序项 HTML
            const sortItemsHtml = tabOrder.map((key, idx) => `
                <div class="lda-sort-item" draggable="true" data-key="${key}">
                    <div class="lda-sort-handle">
                        <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
                    </div>
                    <span class="lda-sort-num">${idx + 1}</span>
                    <span class="lda-sort-label">${tabNames[key]}</span>
                </div>
            `).join('');

            // 支持选项配置
            const supportTiers = [
                { id: 1, amount: 2, icon: '☕', url: 'https://credit.linux.do/paying/online?token=01fdff1ae667a2625d225191717e3281c600218c2152340a4fcd56d7c4423579' },
                { id: 2, amount: 5, icon: '🍵', url: 'https://credit.linux.do/paying/online?token=1f2ceff6ef0bad81cb09c45e03d5bad3c3f71085f6541f56a3de5f20f9c70800' },
                { id: 3, amount: 10, icon: '🍰', url: 'https://credit.linux.do/paying/online?token=cbb30b6eb01e4de09ba1cdba487d4d19a1c6639095d089acd41682f6e9639bc2' },
                { id: 4, amount: 20, icon: '🎂', url: 'https://credit.linux.do/paying/online?token=10fc4d4c07d8073894b1c9654da43da004df5d33a0251dd44ede2199b104373d' }
            ];
            const supportCardsHtml = supportTiers.map(t => `
                <a href="${t.url}" target="_blank" class="lda-support-card tier-${t.id}" rel="noopener">
                    <span class="lda-support-icon">${t.icon}</span>
                    <span class="lda-support-amount">${t.amount}</span>
                    <span class="lda-support-unit">LDC</span>
                </a>
            `).join('');

            // ✅ 隐藏“清除缓存”功能入口:不再渲染按钮(保留内部逻辑以备将来启用)
            this.dom.setting.innerHTML = Utils.html`
                <div class="lda-card">
                    <div class="lda-opt">
                        <div style="display:flex;align-items:center;gap:8px;">
                            <label class="lda-switch"><input type="checkbox" id="inp-expand" ${expand ? 'checked' : ''}><span class="lda-slider"></span></label>
                            <div class="lda-opt-label" style="font-size:12px">${this.t('set_auto')}</div>
                        </div>
                        <div class="lda-seg" id="grp-lang">
                            <div class="lda-seg-item ${r('zh', lang)}" data-v="zh">中文</div>
                            <div class="lda-seg-item ${r('en', lang)}" data-v="en">EN</div>
                        </div>
                    </div>
                    <div class="lda-opt">
                        <div class="lda-opt-label">${this.t('set_size')}</div>
                        <div class="lda-opt-right">
                            <div class="lda-seg" id="grp-size">
                                <div class="lda-seg-item ${r('sm', height)}" data-v="sm">${this.t('size_sm')}</div>
                                <div class="lda-seg-item ${r('lg', height)}" data-v="lg">${this.t('size_lg')}</div>
                                <div class="lda-seg-item ${r('auto', height)}" data-v="auto">${this.t('size_auto')}</div>
                            </div>
                            <div class="lda-opacity-row">
                                <span class="lda-opt-sub">${this.t('set_opacity')}</span>
                                <input type="range" min="0.5" max="1" step="0.05" value="${opacityVal}" id="inp-opacity" class="lda-range">
                                <span id="val-opacity">${opacityPercent}%</span>
                            </div>
                        </div>
                    </div>
                    <div class="lda-opt">
                        <div>
                            <div class="lda-opt-label">${this.t('set_refresh')}</div>
                            <div style="font-size:11px;color:var(--lda-dim);margin-top:4px;">${this.t('refresh_tip')}</div>
                        </div>
                        <div class="lda-seg" id="grp-refresh">
                            <div class="lda-seg-item ${r(30, refreshInterval)}" data-v="30">${this.t('refresh_30')}</div>
                            <div class="lda-seg-item ${r(60, refreshInterval)}" data-v="60">${this.t('refresh_60')}</div>
                            <div class="lda-seg-item ${r(120, refreshInterval)}" data-v="120">${this.t('refresh_120')}</div>
                            <div class="lda-seg-item ${r(0, refreshInterval)}" data-v="0">${this.t('refresh_off')}</div>
                        </div>
                    </div>
                    <div class="lda-opt" style="flex-direction:column; align-items:stretch;">
                        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
                            <div class="lda-opt-label">${this.t('set_tab_order')}</div>
                            <span style="font-size:10px; color:var(--lda-dim)">${this.t('tab_order_tip')}</span>
                        </div>
                        <div class="lda-sortable" id="sortable-tabs">
                            ${sortItemsHtml}
                        </div>
                        <button class="lda-sort-btn" id="btn-save-order">${this.t('tab_order_save')}</button>
                    </div>
                </div>
                <div class="lda-support">
                    <div class="lda-support-header">
                        <div class="lda-support-title">
                            <span class="lda-support-heart">💖</span>
                            ${this.t('support_title')}
                        </div>
                        <div class="lda-support-desc">${this.t('support_desc')}</div>
                    </div>
                    <div class="lda-support-grid">
                        ${supportCardsHtml}
                    </div>
                </div>
                <div style="text-align:center; margin-top:8px;">
                    <div style="font-size:10px; color:var(--lda-dim); opacity:0.6;">
                        v${GM_info.script.version} &bull; By [email protected]
                    </div>
                </div>
            `;

            this.initSortable();
        }

        initSortable() {
            const container = Utils.el('#sortable-tabs', this.dom.setting);
            const saveBtn = Utils.el('#btn-save-order', this.dom.setting);
            let draggedItem = null;

            // 更新序号显示
            const updateNumbers = () => {
                const items = container.querySelectorAll('.lda-sort-item');
                items.forEach((item, idx) => {
                    item.querySelector('.lda-sort-num').textContent = idx + 1;
                });
            };

            // 拖拽开始
            container.addEventListener('dragstart', (e) => {
                if (e.target.classList.contains('lda-sort-item')) {
                    draggedItem = e.target;
                    e.target.classList.add('dragging');
                    e.dataTransfer.effectAllowed = 'move';
                }
            });

            // 拖拽结束
            container.addEventListener('dragend', (e) => {
                if (e.target.classList.contains('lda-sort-item')) {
                    e.target.classList.remove('dragging');
                    container.querySelectorAll('.lda-sort-item').forEach(item => {
                        item.classList.remove('drag-over');
                    });
                    draggedItem = null;
                    updateNumbers();
                }
            });

            // 拖拽经过
            container.addEventListener('dragover', (e) => {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                const target = e.target.closest('.lda-sort-item');
                if (target && target !== draggedItem) {
                    container.querySelectorAll('.lda-sort-item').forEach(item => {
                        item.classList.remove('drag-over');
                    });
                    target.classList.add('drag-over');
                }
            });

            // 放置
            container.addEventListener('drop', (e) => {
                e.preventDefault();
                const target = e.target.closest('.lda-sort-item');
                if (target && target !== draggedItem && draggedItem) {
                    const items = [...container.querySelectorAll('.lda-sort-item')];
                    const draggedIdx = items.indexOf(draggedItem);
                    const targetIdx = items.indexOf(target);

                    if (draggedIdx < targetIdx) {
                        target.after(draggedItem);
                    } else {
                        target.before(draggedItem);
                    }
                    updateNumbers();
                }
            });

            // 保存按钮
            saveBtn.onclick = (e) => {
                e.stopPropagation();
                const wasOpen = this.dom.panel.style.display === 'flex';
                const items = container.querySelectorAll('.lda-sort-item');
                const newOrder = [...items].map(item => item.dataset.key);
                this.state.tabOrder = newOrder;
                Utils.set(CONFIG.KEYS.TAB_ORDER, newOrder);

                // 显示保存成功
                saveBtn.textContent = this.t('tab_order_saved');
                saveBtn.classList.add('saved');
                setTimeout(() => {
                    saveBtn.textContent = this.t('tab_order_save');
                    saveBtn.classList.remove('saved');
                }, 1500);

                // 重新渲染应用新顺序
                this.dom.root.remove();
                this.init(wasOpen);
            };
        }

        bindGlobalEvents() {
            Utils.el('#lda-btn-close').onclick = () => this.togglePanel(false);
            Utils.el('#lda-btn-update').onclick = (e) => { e.stopPropagation(); this.checkUpdate({ isAuto: false, force: true }); };

            // 点击页面其他地方收起面板
            document.addEventListener('click', (e) => {
                if (!this.dom.root.contains(e.target) && this.dom.panel.style.display === 'flex') {
                    this.togglePanel(false);
                }
            });

            this.dom.tabs.forEach(t => t.onclick = () => {
                this.dom.tabs.forEach(x => x.classList.remove('active'));
                Utils.els('.lda-page', this.dom.root).forEach(x => x.classList.remove('active'));
                t.classList.add('active');
                Utils.el(`#page-${t.dataset.target}`, this.dom.root).classList.add('active');
                this.activePage = t.dataset.target;
                this.refreshSlowTipForPage(this.activePage);
            });

            this.dom.setting.onclick = (e) => {
                e.stopPropagation();
                const wasOpen = this.dom.panel.style.display === 'flex';
                const langNode = e.target.closest('#grp-lang .lda-seg-item');
                if (langNode && langNode.dataset.v !== this.state.lang) {
                    this.state.lang = langNode.dataset.v;
                    Utils.set(CONFIG.KEYS.LANG, this.state.lang);
                    this.dom.root.remove();
                    this.init(wasOpen);
                    return;
                }
                const sizeNode = e.target.closest('#grp-size .lda-seg-item');
                if (sizeNode) {
                    this.state.height = sizeNode.dataset.v;
                    Utils.set(CONFIG.KEYS.HEIGHT, this.state.height);
                    this.applyHeight();
                    this.renderSettings();
                }
                const refreshNode = e.target.closest('#grp-refresh .lda-seg-item');
                if (refreshNode) {
                    this.state.refreshInterval = Number(refreshNode.dataset.v);
                    Utils.set(CONFIG.KEYS.REFRESH_INTERVAL, this.state.refreshInterval);
                    this.renderSettings();
                    this.startAutoRefreshTimer();
                }
                if (e.target.id === 'inp-expand') {
                    this.state.expand = e.target.checked;
                    Utils.set(CONFIG.KEYS.EXPAND, e.target.checked);
                }
                if (wasOpen) this.togglePanel(true);
            };

            this.dom.setting.addEventListener('input', (e) => {
                if (e.target.id === 'inp-opacity') {
                    e.stopPropagation();
                    const val = Math.max(0.5, Math.min(1, Number(e.target.value) || 1));
                    this.state.opacity = val;
                    Utils.set(CONFIG.KEYS.OPACITY, val);
                    this.applyOpacity();
                    const display = Utils.el('#val-opacity', this.dom.setting);
                    if (display) display.textContent = `${Math.round(val * 100)}%`;
                }
            });

            this.dom.themeBtn.onclick = (e) => {
                e.stopPropagation();
                const wasOpen = this.dom.panel.style.display === 'flex';
                const modes = ['auto', 'light', 'dark'];
                this.state.theme = modes[(modes.indexOf(this.state.theme) + 1) % 3];
                Utils.set(CONFIG.KEYS.THEME, this.state.theme);
                this.applyTheme();
                this.updateThemeIcon();
                if (wasOpen) this.togglePanel(true);
            };

            // 窗口获得焦点时自动刷新(用户授权后回来)
            window.addEventListener('focus', () => this.refreshOnFocusIfNeeded());

            window.addEventListener('resize', () => {
                this.updatePanelSide();
            });

            this.initDrag();
        }

        renderFromCacheAll() {
            if (this.trustData) this.renderTrust(this.trustData);
            if (this.creditData) this.renderCredit(this.creditData);
            if (this.cdkCache?.data) this.renderCDKContent(this.cdkCache.data);
        }

        prewarmAll() {
            // 只在没有缓存数据时才后台刷新,避免重复请求
            if (!this.trustData) this.refreshTrust({ background: true, force: false });
            if (!this.creditData) this.refreshCredit({ background: true, force: false });
            if (!this.cdkCache?.data) this.refreshCDK({ background: true, force: false });
        }

        isPageActive(page) {
            return this.dom.panel?.style.display === 'flex' && this.activePage === page;
        }

        beginWait(page, onlyWhenActive = true) {
            const ps = this.pendingStatus[page];
            ps.count += 1;
            if (!ps.since) ps.since = Date.now();
            const shouldTimer = !onlyWhenActive || this.isPageActive(page);
            if (!ps.timer && shouldTimer) {
                const wait = Math.max(0, 5000 - (Date.now() - ps.since));
                ps.timer = setTimeout(() => this.showSlowTip(page), wait);
            }
            return () => this.finishWait(page, onlyWhenActive);
        }

        finishWait(page, onlyWhenActive = true) {
            const ps = this.pendingStatus[page];
            ps.count = Math.max(0, ps.count - 1);
            if (ps.count === 0) {
                this.clearSlowTip(page);
                ps.since = null;
            }
            if (ps.count > 0 && (!ps.timer) && (!onlyWhenActive || this.isPageActive(page))) {
                const wait = Math.max(0, 5000 - (Date.now() - ps.since));
                ps.timer = setTimeout(() => this.showSlowTip(page), wait);
            }
        }

        showSlowTip(page) {
            const ps = this.pendingStatus[page];
            if (ps.timer) {
                clearTimeout(ps.timer);
                ps.timer = null;
            }
            if (!this.isPageActive(page)) return;
            const el = Utils.el(`.lda-slow-tip[data-page="${page}"]`, this.dom.root);
            if (el) {
                el.textContent = this.t('slow_tip');
                el.style.display = 'block';
            }
            ps.slowShown = true;
        }

        clearSlowTip(page) {
            const ps = this.pendingStatus[page];
            if (ps.timer) {
                clearTimeout(ps.timer);
                ps.timer = null;
            }
            const el = Utils.el(`.lda-slow-tip[data-page="${page}"]`, this.dom.root);
            if (el) el.style.display = 'none';
            ps.slowShown = false;
        }

        refreshSlowTipForPage(page) {
            const ps = this.pendingStatus[page];
            if (ps.count > 0) {
                const wait = Math.max(0, 5000 - (Date.now() - (ps.since || Date.now())));
                if (ps.timer) clearTimeout(ps.timer);
                ps.timer = setTimeout(() => this.showSlowTip(page), wait);
            } else {
                this.clearSlowTip(page);
            }
        }

        makeUserSig(info) {
            if (!info) return null;
            if (info.username) return `uname:${info.username}`;
            if (info.user?.username) return `uname:${info.user.username}`;
            if (info.user_id || info.id) return `uid:${info.user_id || info.id}`;
            return null;
        }

        ensureUserSig(sig) {
            if (!sig) {
                // 退出登录或无法识别用户:清空与账号相关的缓存,避免“旧账号数据残留”
                if (this.userSig) {
                    this.trustData = null;
                    this.creditData = null;
                    this.cdkCache = null;
                    this.state.trustCache = {};
                    this.lastFetch = { trust: 0, credit: 0, cdk: 0 };
                    Utils.set(CONFIG.KEYS.CACHE_TRUST, {});
                    Utils.set(CONFIG.KEYS.CACHE_TRUST_DATA, null);
                    Utils.set(CONFIG.KEYS.CACHE_CREDIT_DATA, null);
                    Utils.set(CONFIG.KEYS.CACHE_CDK, null);
                    Utils.set(CONFIG.KEYS.CACHE_META, this.lastFetch);
                }
                this.userSig = null;
                Utils.set(CONFIG.KEYS.USER_SIG, null);
                return;
            }
            if (this.userSig && this.userSig !== sig) {
                // 账号切换:清空与账号相关的缓存(参考 v4 策略)
                this.trustData = null;
                this.creditData = null;
                this.cdkCache = null;
                this.state.trustCache = {};
                this.lastFetch = { trust: 0, credit: 0, cdk: 0 };
                Utils.set(CONFIG.KEYS.CACHE_TRUST, {});
                Utils.set(CONFIG.KEYS.CACHE_TRUST_DATA, null);
                Utils.set(CONFIG.KEYS.CACHE_CREDIT_DATA, null);
                Utils.set(CONFIG.KEYS.CACHE_CDK, null);
                Utils.set(CONFIG.KEYS.CACHE_META, this.lastFetch);
            }
            this.userSig = sig;
            Utils.set(CONFIG.KEYS.USER_SIG, sig);
        }


        ensureStorageSchema() {
            const ver = Utils.get(CONFIG.KEYS.CACHE_SCHEMA, 0);
            if (ver !== CONFIG.CACHE_SCHEMA_VERSION) {
                // 缓存结构变更/旧版本残留:仅清空“数据缓存”,保留用户设置(主题、位置等)
                this.trustData = null;
                this.creditData = null;
                this.cdkCache = null;
                this.state.trustCache = {};
                this.lastFetch = { trust: 0, credit: 0, cdk: 0 };
                Utils.set(CONFIG.KEYS.CACHE_TRUST, {});
                Utils.set(CONFIG.KEYS.CACHE_TRUST_DATA, null);
                Utils.set(CONFIG.KEYS.CACHE_CREDIT_DATA, null);
                Utils.set(CONFIG.KEYS.CACHE_CDK, null);
                Utils.set(CONFIG.KEYS.CACHE_META, this.lastFetch);
                Utils.set(CONFIG.KEYS.CACHE_SCHEMA, CONFIG.CACHE_SCHEMA_VERSION);
            }
        }

        validateLoadedCache() {
            // lastFetch 兜底
            if (!this.lastFetch || typeof this.lastFetch !== 'object') {
                this.lastFetch = { trust: 0, credit: 0, cdk: 0 };
            } else {
                ['trust', 'credit', 'cdk'].forEach(k => {
                    if (!Number.isFinite(this.lastFetch[k])) this.lastFetch[k] = 0;
                });
            }

            // trustData 结构校验
            const basic = this.trustData?.basic;
            const trustOk = !!(basic && basic.level !== undefined && Array.isArray(basic.items));
            if (!trustOk) {
                this.trustData = null;
                this.lastFetch.trust = 0;
                Utils.set(CONFIG.KEYS.CACHE_TRUST_DATA, null);
            }

            // creditData 结构校验(避免旧缓存导致“尚未登录/需授权”误判)
            const info = this.creditData?.info;
            const creditOk = !!(info && info.available_balance !== undefined && info.remain_quota !== undefined);
            if (!creditOk) {
                this.creditData = null;
                this.lastFetch.credit = 0;
                Utils.set(CONFIG.KEYS.CACHE_CREDIT_DATA, null);
            }

            // cdkCache 结构校验(兼容旧缓存直接存 data 的情况)
            const cdkOk = !!(this.cdkCache && typeof this.cdkCache === 'object' && this.cdkCache.data && Number.isFinite(this.cdkCache.ts));
            if (!cdkOk) {
                if (this.cdkCache && typeof this.cdkCache === 'object' && !('data' in this.cdkCache) && !('ts' in this.cdkCache)) {
                    this.cdkCache = { ts: 0, data: this.cdkCache };
                    Utils.set(CONFIG.KEYS.CACHE_CDK, this.cdkCache);
                } else {
                    this.cdkCache = null;
                    this.lastFetch.cdk = 0;
                    Utils.set(CONFIG.KEYS.CACHE_CDK, null);
                }
            }

            Utils.set(CONFIG.KEYS.CACHE_META, this.lastFetch);
        }


        startUserWatcher() {
            // 借鉴 v4:每 5 秒检查一次账号是否切换/退出,用于缓存失效与 UI 更新
            if (this.userWatchTimer) return;
            if (location.host !== 'linux.do') return;

            const tick = () => {
                const username = Utils.getCurrentUsernameFromDOM() || Utils.getCurrentUsername();
                const loginState = Utils.getLoginStateByDOM();

                if (username) {
                    const sig = this.makeUserSig({ username });
                    if (sig && this.userSig !== sig) {
                        this.ensureUserSig(sig);
                        // 账号切换后:立刻刷新缓存与 UI(面板开着时体验更好)
                        this.renderFromCacheAll();
                        this.prewarmAll();
                    }
                } else if (loginState === false) {
                    // 已明确退出登录
                    if (this.userSig) {
                        this.ensureUserSig(null);
                        this.renderFromCacheAll();
                    }
                }
            };

            tick();
            this.userWatchTimer = setInterval(tick, 5000);
        }

        isExpired(type) {
            const minutes = Number.isFinite(this.state.refreshInterval) ? this.state.refreshInterval : 30;
            if (minutes <= 0) return false;
            const interval = minutes * 60 * 1000;
            return (Date.now() - (this.lastFetch[type] || 0)) > interval;
        }

        markFetch(type) {
            this.lastFetch[type] = Date.now();
            Utils.set(CONFIG.KEYS.CACHE_META, this.lastFetch);
        }

        maybeAutoCheckUpdate() {
            const now = Date.now();
            const ONE_HOUR = 60 * 60 * 1000;
            if (now - (this.lastSkipUpdate || 0) < ONE_HOUR) return;
            this.checkUpdate({ isAuto: true });
        }

        // ✅ 新:通用友好状态卡片(用于网络错误/环境问题等)
        renderStateCard(wrap, page, {
            title,
            tip,
            levelText = null,
            leftUrl = null,
            leftText = null,
            onRetry = null
        }) {
            this.focusFlags[page] = true;

            const lvlHtml = levelText !== null && levelText !== undefined
                ? `<div style="display:flex;justify-content:center;gap:8px;align-items:center;margin-bottom:10px;">
                        <span class="lda-big-lvl" style="font-size:18px;line-height:1;">Lv.${String(levelText)}</span>
                   </div>`
                : '';

            const leftBtnHtml = leftUrl
                ? `<a href="${leftUrl}" target="_blank" rel="noopener"
                        class="lda-auth-btn secondary" id="btn-go-${page}">
                        ${leftText || this.t('link_tip')} →
                   </a>`
                : '';

            wrap.innerHTML = `
                <div class="lda-card lda-auth-card">
                    <div class="lda-auth-icon">
                        <svg viewBox="0 0 24 24" width="48" height="48">
                            <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
                        </svg>
                    </div>
                    ${lvlHtml}
                    <div class="lda-auth-title">${title}</div>
                    <div class="lda-auth-tip">${tip}</div>
                    <div class="lda-auth-btns">
                        ${leftBtnHtml}
                        <button class="lda-auth-btn" id="btn-retry-${page}">${this.t('network_error_retry')}</button>
                    </div>
                </div>
            `;

            const retryBtn = Utils.el(`#btn-retry-${page}`, wrap);
            if (retryBtn) retryBtn.onclick = (e) => {
                e.stopPropagation();
                onRetry?.();
            };

            const goBtn = Utils.el(`#btn-go-${page}`, wrap);
            if (goBtn) goBtn.onclick = () => {
                // 用户跳转出去再回来时刷新当页
                this.focusFlags[page] = true;
            };
        }

        // ===== 新增:刷新按钮状态管理 =====
        setRefreshBtnLoading(page, on) {
            const btnMap = { trust: '#btn-re-trust', credit: '#btn-re-credit', cdk: '#btn-re-cdk' };
            const selector = btnMap[page];
            if (!selector) return;
            const wrap = this.dom[page];
            if (!wrap) return;
            const btn = Utils.el(selector, wrap);
            if (btn) {
                btn.classList.toggle('loading', on);
            }
        }

        stopRefreshWithMinDuration(page, minDuration = 1000) {
            if (!this.refreshingPages[page] && !this.refreshStopPending[page]) return;
            if (this.refreshStopPending[page]) return;

            const elapsed = Date.now() - (this.refreshStartTime[page] || 0);
            const remaining = Math.max(0, minDuration - elapsed);

            const doStop = () => {
                this.refreshingPages[page] = false;
                this.refreshStopPending[page] = false;
                this.setRefreshBtnLoading(page, false);
            };

            if (remaining > 0) {
                this.refreshStopPending[page] = true;
                setTimeout(doStop, remaining);
            } else {
                doStop();
            }
        }

        // ===== 信任等级:Summary 快照(用于 lv2+ Connect 失败 fallback,以及 lv0-1 失败时的补救展示)=====
        async fetchSummaryTrustSnapshot(username, levelNum) {
            const summary = await Utils.fetchUserSummary(username);
            if (!summary) throw new Error('SummaryError');

            const fields = [
                { key: 'days_visited', nameZh: '访问天数', nameEn: 'Days visited' },
                { key: 'topics_entered', nameZh: '浏览的话题', nameEn: 'Topics entered' },
                { key: 'posts_read_count', nameZh: '已读帖子', nameEn: 'Posts read' },
                { key: 'likes_given', nameZh: '送出赞', nameEn: 'Likes given' },
                { key: 'likes_received', nameZh: '获赞', nameEn: 'Likes received' },
                { key: 'time_read', nameZh: '阅读时间', nameEn: 'Time read', unit: 'seconds' }
            ];

            const req = CONFIG.LEVEL_REQUIREMENTS[levelNum] || null;

            const items = [];
            let allPassed = true;
            const newCache = {};

            for (const f of fields) {
                const displayName = this.state.lang === 'zh' ? f.nameZh : f.nameEn;
                let currentRaw = summary[f.key] || 0;
                let currentDisplay = String(currentRaw);

                if (f.unit === 'seconds') currentDisplay = Utils.formatReadTime(currentRaw);

                let target = null;
                let targetDisplay = '-';
                if (req?.[f.key]?.target !== undefined) {
                    target = req[f.key].target;
                    targetDisplay = (req[f.key].unit === 'seconds')
                        ? Utils.formatReadTime(target)
                        : String(target);
                }

                let isGood = null;
                let pct = 0;

                if (target !== null) {
                    isGood = currentRaw >= target;
                    pct = target > 0 ? Math.min((currentRaw / target) * 100, 100) : (isGood ? 100 : 0);
                    if (!isGood) allPassed = false;
                } else {
                    // 无目标时:中性展示
                    isGood = null;
                    pct = 0;
                }

                const oldVal = this.state.trustCache[displayName];
                let diff = 0;
                if (typeof oldVal === 'number' && oldVal !== currentRaw) diff = currentRaw - oldVal;

                newCache[displayName] = currentRaw;

                items.push({
                    name: displayName,
                    current: currentDisplay,
                    target: targetDisplay,
                    isGood,
                    pct,
                    diff
                });
            }

            // 仍然缓存当前值用于 diff(即使是 fallback)
            this.state.trustCache = newCache;
            Utils.set(CONFIG.KEYS.CACHE_TRUST, newCache);

            return {
                level: String(levelNum ?? '?'),
                isPass: targetAny(req) ? allPassed : null,
                items,
                source: 'summary'
            };

            function targetAny(r) {
                if (!r) return false;
                return Object.values(r).some(x => x?.target !== undefined);
            }
        }

        // ===== 0-1级用户数据获取(使用 summary.json + 硬编码要求)=====
        async fetchLowLevelTrustData(username, currentLevel) {
            const summary = await Utils.fetchUserSummary(username);
            if (!summary) throw new Error("ParseError");

            const requirements = CONFIG.LEVEL_REQUIREMENTS[currentLevel];
            if (!requirements) throw new Error("ParseError");

            const items = [];
            const newCache = {};
            let allPassed = true;

            for (const [key, req] of Object.entries(requirements)) {
                let current = summary[key] || 0;
                let target = req.target;
                let currentDisplay = String(current);

                // 处理时间格式
                if (req.unit === 'seconds') {
                    currentDisplay = Utils.formatReadTime(current);
                }

                const isGood = current >= target;
                if (!isGood) allPassed = false;

                const oldVal = this.state.trustCache[req.name];
                let diff = 0;
                if (typeof oldVal === 'number' && oldVal !== current) {
                    diff = current - oldVal;
                }

                newCache[req.name] = current;

                let pct = target > 0 ? Math.min((current / target) * 100, 100) : (isGood ? 100 : 0);

                let targetDisplay = req.unit === 'seconds' ? Utils.formatReadTime(target) : target;

                items.push({
                    name: req.name,
                    current: currentDisplay,
                    target: targetDisplay,
                    isGood,
                    pct,
                    diff
                });
            }

            this.state.trustCache = newCache;
            Utils.set(CONFIG.KEYS.CACHE_TRUST, newCache);

            return {
                level: String(currentLevel),
                isPass: allPassed,
                items,
                source: 'summary'
            };
        }

        // ===== 2级及以上用户数据获取(使用 connect.linux.do)=====
        async fetchHighLevelTrustData(knownLevel = null) {
            const html = await Utils.request(CONFIG.API.TRUST);
            const doc = new DOMParser().parseFromString(html, 'text/html');
            const bodyText = doc.body?.textContent || '';
            const loginHint = doc.querySelector('a[href*="/login"], form[action*="/login"], form[action*="/session"]');
            const levelNode = Array.from(doc.querySelectorAll('h1, h2, h3')).find(x => /信任|trust/i.test(x.textContent));

            if (!levelNode) {
                const possibleTable = doc.querySelector('table');
                if (loginHint || /登录|login|sign\s*in/i.test(bodyText)) throw new Error("NeedLogin");
                if (!possibleTable) throw new Error("ParseError");
            }

            const level = knownLevel !== null ? String(knownLevel) : levelNode.textContent.replace(/\D/g, '');
            const rows = Array.from(levelNode.parentElement.parentElement.querySelectorAll('tr')).slice(1);
            if (rows.length === 0) this.focusFlags.trust = true;

            const items = [];
            const newCache = {};
            const seenNames = {};
            let allPassed = true;

            rows.forEach(tr => {
                const tds = tr.querySelectorAll('td');
                if (tds.length < 3) return;

                let name = tds[0].textContent.trim().split('(')[0];
                const current = parseFloat(tds[1].textContent.replace(/,/g, ''));
                const target = parseFloat(tds[2].textContent.replace(/,/g, ''));
                const isGood = tds[1].classList.contains('text-green-500');

                if (!isGood) allPassed = false;

                if (seenNames[name]) {
                    name = name + ' (All)';
                }
                seenNames[name] = true;

                const oldVal = this.state.trustCache[name];
                let diff = 0;
                if (typeof oldVal === 'number' && oldVal !== current) {
                    diff = current - oldVal;
                }

                newCache[name] = current;

                let pct = 0;
                if (target > 0) pct = Math.min((current / target) * 100, 100);
                else if (isGood) pct = 100;

                items.push({ name, current: tds[1].textContent.trim(), target, isGood, pct, diff });
            });

            this.state.trustCache = newCache;
            Utils.set(CONFIG.KEYS.CACHE_TRUST, newCache);

            // 只有当存在数据行且所有项目都达标时,isPass 才为 true
            const isPass = items.length > 0 && allPassed;

            return { level, isPass, items, source: 'connect' };
        }

        // 生成降级提示横幅HTML
        getFallbackBannerHtml() {
            return `
                <div class="lda-fallback-banner">
                    <svg viewBox="0 0 24 24"><path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>
                    <div class="lda-fallback-text">
                        <strong>${this.t('trust_fallback_title')}</strong><br>
                        ${this.t('trust_fallback_tip')}
                    </div>
                </div>
            `;
        }

        // 生成数据来源标签HTML
        getSourceTagHtml(source) {
            const sourceText = source === 'connect' ? 'Connect' : 'Summary';
            return `<span class="lda-source-tag">${this.t('trust_data_source')}: ${sourceText}</span>`;
        }

        // ===================== 信任级别刷新(按你要求的状态机重构) =====================
        async refreshTrust(arg = true) {
            const base = { background: false, force: undefined, manual: false };
            const opts = typeof arg === 'object' ? { ...base, ...arg } : { ...base, manual: !!arg, force: arg === false ? false : undefined };
            const manual = opts.manual;
            const forceFetch = opts.force ?? !opts.background;
            const wrap = this.dom.trust;
            const endWait = this.beginWait('trust');

            // 旋转
            this.refreshingPages.trust = true;
            this.refreshStartTime.trust = Date.now();
            this.setRefreshBtnLoading('trust', true);

            if (!wrap.innerHTML || wrap.innerHTML.trim() === '') {
                wrap.innerHTML = `<div style="text-align:center;padding:30px;color:var(--lda-dim)">${this.t('loading')}</div>`;
            }

            try {
                if (this.trustData && !forceFetch && !this.isExpired('trust')) {
                    this.renderTrust(this.trustData);
                    this.stopRefreshWithMinDuration('trust');
                    endWait();
                    return;
                }
                if (this.trustData) this.renderTrust(this.trustData);

                // ✅ 1) 登录态 / 用户名 / 当前等级 获取(多策略兜底:session → connect → DOM)
                const sessionUser = await Utils.fetchSessionUser();

                let username = sessionUser?.username
                    || Utils.getCurrentUsername()
                    || Utils.getCurrentUsernameFromDOM();

                let userTrustLevel = Number.isFinite(sessionUser?.trust_level) ? sessionUser.trust_level : null;

                // 兜底:从 connect.linux.do 欢迎语解析(参考 v4)
                if (!username || userTrustLevel === null) {
                    try {
                        const cw = await Utils.fetchConnectWelcome();
                        if (!username && cw?.username) username = cw.username;
                        if (userTrustLevel === null && cw?.trustLevel !== null && cw?.trustLevel !== undefined) userTrustLevel = cw.trustLevel;
                    } catch (_) {
                        // ignore(NeedLogin / parse error 将在下面处理)
                    }
                }

                // ✅ 未能拿到用户名:更谨慎的登录判断(session 不可用时,使用 DOM 是否存在“登录/注册”入口作为辅助)
                if (!username) {
                    const domState = Utils.getLoginStateByDOM();
                    if (domState === false) {
                        this.focusFlags.trust = true;
                        wrap.innerHTML = `
          <div class="lda-card lda-auth-card">
            <div class="lda-auth-top">
              <div class="lda-auth-icon">
                <svg viewBox="0 0 24 24" width="22" height="22" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path d="M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4Z" fill="currentColor" opacity=".15"/>
                  <path d="M12 2.2 20 5.8v5.2c0 4.95-3.33 9.58-8 10.86C7.33 20.58 4 15.95 4 11V5.8l8-3.6Z" stroke="currentColor" stroke-width="1.2"/>
                  <path d="M9.4 12.4 11 14l3.6-3.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
                </svg>
              </div>
              <div>
                <div style="font-weight:800">${this.t('trust')}</div>
                <div style="font-size:12px;color:var(--lda-dim);margin-top:2px;line-height:1.5">${this.t('trust_login_tip')}</div>
              </div>
            </div>
            <div class="lda-auth-actions">
              <button class="lda-auth-btn primary" id="btn-go-login">${this.t('trust_go_login')}</button>
              <button class="lda-auth-btn secondary" id="btn-retry-trust">${this.t('refresh')}</button>
            </div>
          </div>`;
                        const btn = Utils.el('#btn-go-login', wrap);
                        if (btn) btn.onclick = () => location.href = '/login';
                        const retry = Utils.el('#btn-retry-trust', wrap);
                        if (retry) retry.onclick = (e) => { e.stopPropagation(); this.refreshTrust(true); };
                        this.stopRefreshWithMinDuration('trust');
                        endWait();
                        return;
                    }

                    // DOM 无法确认:仍给出“刷新/前往登录”的入口
                    this.focusFlags.trust = true;
                    wrap.innerHTML = `
          <div class="lda-card lda-auth-card">
            <div class="lda-auth-top">
              <div class="lda-auth-icon">
                <svg viewBox="0 0 24 24" width="22" height="22" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path d="M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4Z" fill="currentColor" opacity=".15"/>
                  <path d="M12 2.2 20 5.8v5.2c0 4.95-3.33 9.58-8 10.86C7.33 20.58 4 15.95 4 11V5.8l8-3.6Z" stroke="currentColor" stroke-width="1.2"/>
                  <path d="M9.4 12.4 11 14l3.6-3.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
                </svg>
              </div>
              <div>
                <div style="font-weight:800">${this.t('trust')}</div>
                <div style="font-size:12px;color:var(--lda-dim);margin-top:2px;line-height:1.5">
                  ${this.t('trust_login_tip')}
                </div>
              </div>
            </div>
            <div class="lda-auth-actions">
              <button class="lda-auth-btn primary" id="btn-go-login">${this.t('trust_go_login')}</button>
              <button class="lda-auth-btn secondary" id="btn-retry-trust">${this.t('refresh')}</button>
            </div>
          </div>`;
                    const btn = Utils.el('#btn-go-login', wrap);
                    if (btn) btn.onclick = () => location.href = '/login';
                    const retry = Utils.el('#btn-retry-trust', wrap);
                    if (retry) retry.onclick = (e) => { e.stopPropagation(); this.refreshTrust(true); };
                    this.stopRefreshWithMinDuration('trust');
                    endWait();
                    return;
                }

                // ✅ 2) 已登录:拿 username + trust_level
                if (userTrustLevel === null && username) {
                    const ui = await Utils.fetchUserInfo(username);
                    userTrustLevel = ui?.trust_level ?? null;
                }
                const levelText = userTrustLevel ?? '?';
                if (username) this.ensureUserSig(this.makeUserSig({ username }));

                // 并行获取排名数据(失败不阻断)
                const statsPromise = Utils.fetchForumStats().catch(() => null);

                // ✅ 3) 状态机:
                //  - lv0-1:summary 成功 => 正常;失败 => 友好错误 UI(显示具体等级)
                //  - lv2+:connect 成功 => 正常;connect失败但summary成功 => fallback(提示+左connect右刷新);都失败 => 友好错误 UI(左connect右刷新)
                let basic = null;

                if (userTrustLevel !== null && userTrustLevel <= 1) {
                    try {
                        basic = await this.fetchLowLevelTrustData(username, userTrustLevel);
                        basic.ui = 'normal';
                    } catch (_) {
                        this.renderStateCard(wrap, 'trust', {
                            title: this.t('network_error_title'),
                            tip: this.t('network_error_tip'),
                            levelText,
                            leftUrl: null,
                            onRetry: () => this.refreshTrust({ manual: true, force: true })
                        });
                        this.stopRefreshWithMinDuration('trust');
                        endWait();
                        return;
                    }
                } else {
                    // lv2+ 先 connect
                    try {
                        basic = await this.fetchHighLevelTrustData(userTrustLevel);
                        basic.ui = 'normal';
                    } catch (e) {
                        basic = null;
                    }

                    // connect 失败 => summary fallback
                    if (!basic) {
                        try {
                            const snap = await this.fetchSummaryTrustSnapshot(username, userTrustLevel ?? 2);
                            basic = {
                                ...snap,
                                ui: 'fallback',
                                // 对齐字段
                                isPass: snap.isPass
                            };
                        } catch (_) {
                            // connect + summary 都失败 => 友好错误 UI
                            this.renderStateCard(wrap, 'trust', {
                                title: this.t('network_error_title'),
                                tip: this.t('network_error_tip'),
                                levelText,
                                leftUrl: CONFIG.API.LINK_TRUST,
                                leftText: this.t('connect_open'),
                                onRetry: () => this.refreshTrust({ manual: true, force: true })
                            });
                            this.stopRefreshWithMinDuration('trust');
                            endWait();
                            return;
                        }
                    }
                }

                // ✅ 4) 合并 stats
                const forumStats = await statsPromise;
                const statsData = forumStats ? {
                    dailyRank: forumStats.dailyRank || null,
                    globalRank: forumStats.globalRank || null,
                    score: forumStats.score || null
                } : null;

                // ✅ 5) 渲染并缓存
                this.trustData = { basic, stats: statsData };
                this.renderTrust(this.trustData);
                Utils.set(CONFIG.KEYS.CACHE_TRUST_DATA, this.trustData);
                this.markFetch('trust');
                if (manual) this.showToast(this.t('refresh_done'), 'success', 1500);

            } catch (e) {
                // 最外层兜底:当作网络/环境错误,但尽量给 connect + refresh
                this.renderStateCard(wrap, 'trust', {
                    title: this.t('network_error_title'),
                    tip: this.t('network_error_tip'),
                    levelText: '?',
                    leftUrl: CONFIG.API.LINK_TRUST,
                    leftText: this.t('connect_open'),
                    onRetry: () => this.refreshTrust({ manual: true, force: true })
                });
            } finally {
                this.stopRefreshWithMinDuration('trust');
                endWait();
            }
        }

        renderTrust(data) {
            const wrap = this.dom.trust;
            if (!data?.basic) {
                wrap.innerHTML = `<div style="text-align:center;padding:30px;color:var(--lda-dim)">${this.t('loading')}</div>`;
                return;
            }
            const { level, isPass, items, source, ui } = data.basic;
            const stats = data.stats || {};
            const isFallback = ui === 'fallback';
            const showConnectLink = true;

            let statsHtml = '';
            if (stats.globalRank || stats.dailyRank || stats.score) {
                statsHtml = `<a href="${CONFIG.API.LINK_LEADERBOARD}" target="_blank" class="lda-stats-bar" id="btn-go-leaderboard">`;
                if (stats.dailyRank) statsHtml += `<span class="lda-stat-item">${this.t('rank_today')}: <span class="num today">${stats.dailyRank}</span></span>`;
                if (stats.globalRank) statsHtml += `<span class="lda-stat-item">${this.t('rank')}: <span class="num rank">${stats.globalRank}</span></span>`;
                if (stats.score) statsHtml += `<span class="lda-stat-item">${this.t('score')}: <span class="num score">${Number(stats.score).toLocaleString()}</span></span>`;
                statsHtml += `</a>`;
            }

            // 顶部动作区:正常用图标;fallback 用底部大按钮(但保留图标刷新更顺手)
            let actionsHtml = `
                <div class="lda-actions-group">
                    ${showConnectLink ? `
                    <a href="${CONFIG.API.LINK_TRUST}" target="_blank" class="lda-act-btn" title="${this.t('link_tip')}" id="btn-go-connect-icon">
                        <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
                    </a>` : ''}
                    <div class="lda-act-btn" id="btn-re-trust" title="${this.t('refresh_tip_btn')}">
                        <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/></svg>
                    </div>
                </div>
            `;

            let listHtml = '';
            (items || []).forEach(it => {
                let diffHtml = '';
                if (it.diff) {
                    if (it.diff > 0) diffHtml = `<span class="lda-diff up">▲${it.diff}</span>`;
                    else if (it.diff < 0) diffHtml = `<span class="lda-diff down">▼${Math.abs(it.diff)}</span>`;
                }

                // isGood: true/false/null
                let valColor = 'var(--lda-fg)';
                let fillColor = 'var(--lda-neutral)';
                let pct = Number(it.pct) || 0;

                if (it.isGood === true) { valColor = 'var(--lda-green)'; fillColor = 'var(--lda-green)'; }
                else if (it.isGood === false) { valColor = 'var(--lda-red)'; fillColor = 'var(--lda-red)'; }
                else { // null => neutral
                    valColor = 'var(--lda-fg)';
                    fillColor = 'var(--lda-neutral)';
                    pct = 100; // 中性条显示为满,但颜色更淡(表示仅展示统计,无“达标”含义)
                }

                listHtml += Utils.html`
                    <div class="lda-item">
                        <div class="lda-item-top">
                            <span class="lda-i-name">${it.name}</span>
                            <span class="lda-i-val" style="color:${valColor}">
                                ${it.current} ${diffHtml}
                                <span style="color:var(--lda-dim);font-weight:400;margin-left:4px">/ ${it.target ?? '-'}</span>
                            </span>
                        </div>
                        <div class="lda-progress"><div class="lda-fill" style="width:${pct}%; background:${fillColor}"></div></div>
                    </div>
                `;
            });


            const isCelebration = (isPass === true) && !isFallback && (items || []).length > 0;

            let bodyHtml = listHtml;
            if (isCelebration) {
                const lvlNum = Number(level);
                const target = (Number.isFinite(lvlNum) ? String(lvlNum >= 3 ? 3 : (lvlNum + 1)) : '3');
                const msg = (Number.isFinite(lvlNum) && lvlNum >= 3)
                    ? this.t('celebrate_msg_lv3')
                    : this.t('celebrate_msg_upgrade').replace('{level}', target);

                bodyHtml = Utils.html`
                    <div class="lda-celebration-wrap">
                        <div class="lda-celebration-achievement">
                            <div class="lda-celebration-icon">
                                <div class="lda-celebration-ring"></div>
                                <div class="lda-celebration-ring-outer"></div>
                                <svg viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
                                    <path d="M7 4h10v2h2a1 1 0 0 1 1 1v2a5 5 0 0 1-5 5h-1.1A5.002 5.002 0 0 1 13 16.9V19h3v2H8v-2h3v-2.1A5.002 5.002 0 0 1 10.1 14H9A5 5 0 0 1 4 9V7a1 1 0 0 1 1-1h2V4Zm12 3h-2v5h1a3 3 0 0 0 3-3V7ZM7 7H5v2a3 3 0 0 0 3 3h1V7Zm2-3v8a3 3 0 0 0 3 3 3 3 0 0 0 3-3V4H9Z" fill="white"/>
                                </svg>
                            </div>
                            <div class="lda-celebration-title">${this.t('celebrate_title')}</div>
                            <div class="lda-celebration-subtitle">${this.t('celebrate_subtitle')}</div>
                            <div class="lda-celebration-message">${msg}</div>
                        </div>

                        <div class="lda-celebration-details">
                            <div class="lda-celebration-scroll">
                                ${bodyHtml}
                            </div>
                        </div>

                        <div class="lda-celebration-actions">
                            <button class="lda-auth-btn secondary" id="btn-trust-toggle-details">${this.t('btn_details')}</button>
                        </div>
                    </div>
                `;
            }

            const bannerHtml = isFallback ? this.getFallbackBannerHtml() : '';
            const sourceTag = this.getSourceTagHtml(source || 'connect');

            const badgeHtml = isFallback
                ? `<span class="lda-badge neutral">${this.t('status_fallback')}</span>`
                : `<span class="lda-badge ${(isPass === true) ? 'ok' : 'no'}">${(isPass === true) ? this.t('status_ok') : this.t('status_fail')}</span>`;

            const fallbackBtns = isFallback ? `
                <div class="lda-auth-btns" style="margin-top:14px;">
                    <a href="${CONFIG.API.LINK_TRUST}" target="_blank" rel="noopener"
                        class="lda-auth-btn secondary" id="btn-go-connect">${this.t('connect_open')} →</a>
                    <button class="lda-auth-btn" id="btn-retry-trust">${this.t('network_error_retry')}</button>
                </div>
            ` : '';

            wrap.innerHTML = Utils.html`
                <div class="lda-card">
                    ${actionsHtml}
                    <div class="lda-info-header">
                        <div class="lda-lvl-group">
                            <span class="lda-big-lvl">Lv.${level}</span>
                            ${badgeHtml}
                            ${sourceTag}
                        </div>
                    </div>
                    ${bannerHtml}
                    ${statsHtml}
                    ${bodyHtml}
                    ${fallbackBtns}
                </div>
            `;

            // 绑定按钮
            const goIcon = Utils.el('#btn-go-connect-icon', wrap);
            if (goIcon) goIcon.onclick = () => { this.focusFlags.trust = true; };

            Utils.el('#btn-re-trust', wrap).onclick = (e) => {
                e.stopPropagation();
                this.refreshTrust({ manual: true, force: true });
            };

            const goBtn = Utils.el('#btn-go-connect', wrap);
            if (goBtn) goBtn.onclick = () => { this.focusFlags.trust = true; };

            const retry = Utils.el('#btn-retry-trust', wrap);
            if (retry) retry.onclick = (e) => {
                e.stopPropagation();
                this.refreshTrust({ manual: true, force: true });
            };

            const toggle = Utils.el('#btn-trust-toggle-details', wrap);
            if (toggle) {
                toggle.onclick = (e) => {
                    e.stopPropagation();
                    const ach = Utils.el('.lda-celebration-achievement', wrap);
                    const det = Utils.el('.lda-celebration-details', wrap);
                    if (!ach || !det) return;

                    const detHidden = getComputedStyle(det).display === 'none';
                    if (detHidden) {
                        ach.style.display = 'none';
                        det.style.display = 'flex';
                        toggle.textContent = this.t('btn_collapse');
                    } else {
                        det.style.display = 'none';
                        ach.style.display = 'flex';
                        toggle.textContent = this.t('btn_details');
                    }
                };
            }


            // 如果正在刷新或等待延迟停止,保持按钮旋转状态
            if (this.refreshingPages.trust || this.refreshStopPending.trust) {
                this.setRefreshBtnLoading('trust', true);
            }
        }

        // ===================== 积分刷新:按你要求的状态机 =====================
        async refreshCredit(arg = true) {
            const base = { background: false, force: undefined, manual: false, autoRetry: true };
            const opts = typeof arg === 'object' ? { ...base, ...arg } : { ...base, manual: !!arg, force: arg === false ? false : undefined };
            const manual = opts.manual;
            const forceFetch = opts.force ?? !opts.background;
            const wrap = this.dom.credit;
            const endWait = this.beginWait('credit');

            this.refreshingPages.credit = true;
            this.refreshStartTime.credit = Date.now();
            this.setRefreshBtnLoading('credit', true);

            if (!wrap.innerHTML || wrap.innerHTML.trim() === '') {
                wrap.innerHTML = `<div style="text-align:center;padding:30px;color:var(--lda-dim)">${this.t('loading')}</div>`;
            }

            try {
                if (this.creditData && !forceFetch && !this.isExpired('credit')) {
                    this.renderCredit(this.creditData);
                    this.stopRefreshWithMinDuration('credit');
                    endWait();
                    return;
                }
                if (this.creditData) this.renderCredit(this.creditData);

                const infoPromise = Utils.request(CONFIG.API.CREDIT_INFO, { withCredentials: true });
                const statPromise = Utils.request(CONFIG.API.CREDIT_STATS, { withCredentials: true });

                let info = null;
                let stats = [];

                await Promise.all([
                    infoPromise.then(r => {
                        info = JSON.parse(r).data;
                        const sig = this.makeUserSig(info);
                        if (sig) this.ensureUserSig(sig);
                    }),
                    statPromise.then(r => {
                        stats = JSON.parse(r).data || [];
                    })
                ]);

                this.creditData = { info, stats };
                this.renderCredit(this.creditData);
                Utils.set(CONFIG.KEYS.CACHE_CREDIT_DATA, this.creditData);
                this.markFetch('credit');
                this.stopRefreshWithMinDuration('credit');
                if (manual) this.showToast(this.t('refresh_done'), 'success', 1500);
                endWait();
            } catch (e) {
                const isLogin = e?.status === 401 || e?.status === 403 || /unauthorized|not\s*login/i.test(e?.responseText || '');

                if (isLogin) {
                    // 如果已有可用缓存数据:不要强行覆盖成“未登录/需授权”,避免偶发 401 造成闪烁
                    const hasUsableCache = !!(this.creditData?.info && this.creditData.info.available_balance !== undefined);
                    if (hasUsableCache) {
                        this.focusFlags.credit = true;
                        this.showToast(this.t('credit_keep_cache_tip'));
                        try { this.renderCredit(this.creditData); } catch (_) { /* ignore */ }
                        this.stopRefreshWithMinDuration('credit');
                        endWait();
                        return;
                    }

                    this.stopRefreshWithMinDuration('credit');
                    this.focusFlags.credit = true;
                    wrap.innerHTML = `
                        <div class="lda-card lda-auth-card">
                            <div class="lda-auth-icon">
                                <svg viewBox="0 0 24 24" width="48" height="48"><path fill="currentColor" d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M17.13,17C15.92,18.85 14.11,20.24 12,20.92C9.89,20.24 8.08,18.85 6.87,17C6.53,16.5 6.24,16 6,15.47C6,13.82 8.71,12.47 12,12.47C15.29,12.47 18,13.79 18,15.47C17.76,16 17.47,16.5 17.13,17Z"/></svg>
                            </div>
                            <div class="lda-auth-title">${this.t('credit_not_auth')}</div>
                            <div class="lda-auth-tip">${this.t('credit_auth_tip')}</div>
                            <div class="lda-auth-btns">
                                <a href="${CONFIG.API.LINK_CREDIT}" target="_blank" class="lda-auth-btn" id="btn-go-credit">${this.t('credit_go_auth')} →</a>
                                <button id="btn-credit-refresh" class="lda-auth-btn secondary">${this.t('credit_refresh')}</button>
                            </div>
                        </div>
                    `;
                    Utils.el('#btn-credit-refresh', wrap).onclick = (ev) => { ev.stopPropagation(); this.refreshCredit({ manual: true, force: true }); };
                    const go = Utils.el('#btn-go-credit', wrap);
                    if (go) go.onclick = () => { this.focusFlags.credit = true; };
                    endWait();
                    return;
                }

                // ✅ 其他失败:友好网络错误 UI(左Credit右刷新)
                this.renderStateCard(wrap, 'credit', {
                    title: this.t('network_error_title'),
                    tip: this.t('network_error_tip'),
                    leftUrl: CONFIG.API.LINK_CREDIT,
                    leftText: this.t('credit_open'),
                    onRetry: () => this.refreshCredit({ manual: true, force: true })
                });

                this.stopRefreshWithMinDuration('credit');
                endWait();
            }
        }

        renderCredit(data) {
            const wrap = this.dom.credit;
            const info = data?.info;
            if (!info) {
                wrap.innerHTML = `<div style="text-align:center;padding:30px;color:var(--lda-dim)">${this.t('loading')}</div>`;
                return;
            }
            const stats = data.stats || [];
            let listHtml = '';
            if (stats.length === 0) {
                listHtml = `<div style="text-align:center;padding:12px;color:var(--lda-dim);font-size:12px">${this.t('no_rec')}</div>`;
            } else {
                [...stats].reverse().forEach(x => {
                    const date = x.date.slice(5).replace('-', '/');
                    const inc = parseFloat(x.income);
                    const exp = parseFloat(x.expense);
                    if (inc > 0) listHtml += `<div class="lda-row-rec"><span>${date} ${this.t('income')}</span><span class="lda-amt" style="color:var(--lda-red)">+${inc}</span></div>`;
                    if (exp > 0) listHtml += `<div class="lda-row-rec"><span>${date} ${this.t('expense')}</span><span class="lda-amt" style="color:var(--lda-green)">-${exp}</span></div>`;
                });
            }
            wrap.innerHTML = Utils.html`
                <div class="lda-card">
                    <div class="lda-actions-group">
                        <a href="${CONFIG.API.LINK_CREDIT}" target="_blank" class="lda-act-btn" title="${this.t('link_tip')}" id="btn-go-credit-icon">
                            <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
                        </a>
                        <div class="lda-act-btn" id="btn-re-credit" title="${this.t('refresh_tip_btn')}">
                            <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/></svg>
                        </div>
                    </div>
                    <div class="lda-credit-hero">
                        <div class="lda-credit-num">${info.available_balance}</div>
                        <div class="lda-credit-label">${this.t('balance')}</div>
                        <div style="margin-top:6px;font-size:12px;color:var(--lda-dim)">${this.t('daily_limit')}: <span style="font-weight:600;color:var(--lda-fg)">${info.remain_quota}</span></div>
                    </div>
                </div>
                <div class="lda-card">
                    <div style="font-size:11px;font-weight:700;color:var(--lda-dim);margin-bottom:10px;">${this.t('recent')}</div>
                    ${listHtml}
                </div>
            `;
            Utils.el('#btn-re-credit', wrap).onclick = (e) => { e.stopPropagation(); this.refreshCredit({ manual: true, force: true }); };

            const goIcon = Utils.el('#btn-go-credit-icon', wrap);
            if (goIcon) goIcon.onclick = () => { this.focusFlags.credit = true; };

            if (this.refreshingPages.credit || this.refreshStopPending.credit) {
                this.setRefreshBtnLoading('credit', true);
            }
        }

        // ===================== CDK 刷新:按你要求的状态机 =====================
        async refreshCDK(arg = true) {
            const base = { background: false, force: undefined, manual: false };
            const opts = typeof arg === 'object' ? { ...base, ...arg } : { ...base, manual: !!arg, force: arg === false ? false : undefined };
            const manual = opts.manual;
            const wrap = this.dom.cdk;
            const endWait = this.beginWait('cdk');

            this.refreshingPages.cdk = true;
            this.refreshStartTime.cdk = Date.now();
            this.setRefreshBtnLoading('cdk', true);

            if (!wrap.innerHTML || wrap.innerHTML.trim() === '') {
                wrap.innerHTML = `<div style="text-align:center;padding:30px;color:var(--lda-dim)">${this.t('loading')}</div>`;
            }

            // 先展示新鲜缓存
            if (this.isCDKCacheFresh()) {
                this.renderCDKContent(this.cdkCache.data);
                if (!opts.force && !this.isExpired('cdk')) {
                    this.stopRefreshWithMinDuration('cdk');
                    endWait();
                    return;
                }
            }

            let directErr = null;
            let bridgeErr = null;

            // direct
            try {
                const info = await this.fetchCDKDirect();
                this.cacheCDKData(info);
                const sig = this.makeUserSig({ username: info.username, user_id: info.id });
                if (sig) this.ensureUserSig(sig);
                this.renderCDKContent(info);
                this.stopRefreshWithMinDuration('cdk');
                this.markFetch('cdk');
                if (manual) this.showToast(this.t('refresh_done'), 'success', 1500);
                endWait();
                return;
            } catch (e) {
                directErr = e;
            }

            // bridge
            try {
                const info = await this.fetchCDKViaBridge();
                this.cacheCDKData(info);
                const sig = this.makeUserSig({ username: info.username, user_id: info.id });
                if (sig) this.ensureUserSig(sig);
                this.renderCDKContent(info);
                this.stopRefreshWithMinDuration('cdk');
                this.markFetch('cdk');
                if (manual) this.showToast(this.t('refresh_done'), 'success', 1500);
                endWait();
                return;
            } catch (e) {
                bridgeErr = e;
            }

            this.stopRefreshWithMinDuration('cdk');

            // 如果已有缓存,就保持缓存,不覆盖为错误/未登录
            if (this.isCDKCacheFresh()) {
                endWait();
                return;
            }

            const isAuthLike = (err) => {
                if (!err) return false;
                if (err?.status === 401 || err?.status === 403) return true;
                const msg = String(err?.message || '');
                return /unauthorized|401|403|forbidden/i.test(msg);
            };

            // ✅ 状态机:未登录/未授权 vs 其他失败
            if (isAuthLike(directErr)) {
                this.renderCDKAuth();
                endWait();
                return;
            }

            // ✅ 其他失败:友好网络错误 UI(左CDK右刷新)
            this.renderStateCard(wrap, 'cdk', {
                title: this.t('network_error_title'),
                tip: this.t('network_error_tip'),
                leftUrl: CONFIG.API.LINK_CDK,
                leftText: this.t('cdk_open'),
                onRetry: () => this.refreshCDK({ manual: true, force: true })
            });

            endWait();
        }

        refreshOnFocusIfNeeded() {
            if (this.dom.panel.style.display !== 'flex') return;
            const flags = this.focusFlags;
            if (flags.trust) {
                flags.trust = false;
                this.refreshTrust({ force: true });
            }
            if (flags.credit) {
                flags.credit = false;
                this.refreshCredit({ force: true });
            }
            if (flags.cdk) {
                flags.cdk = false;
                this.refreshCDK({ force: true });
            }
        }

        startAutoRefreshTimer() {
            if (this.autoRefreshTimer) {
                clearInterval(this.autoRefreshTimer);
                this.autoRefreshTimer = null;
            }
            const minutesRaw = Number(this.state.refreshInterval);
            const minutes = Number.isFinite(minutesRaw) ? minutesRaw : 30;
            if (minutes <= 0) return;
            const interval = minutes * 60 * 1000;
            this.autoRefreshTimer = setInterval(() => {
                // 只要面板开着,就可后台刷新(原逻辑:beginWait 会控制提示)
                this.refreshTrust({ background: true, force: false });
                this.refreshCredit({ background: true, force: false });
                this.refreshCDK({ background: true, force: false });
            }, interval || AUTO_REFRESH_MS);
        }

        async fetchCDKDirect() {
            const infoRes = await Utils.request(CONFIG.API.CDK_INFO, { withCredentials: true });
            const parsed = JSON.parse(infoRes);
            if (!parsed?.data) throw new Error('no data');
            return parsed.data;
        }

        ensureCDKBridge() {
            if (this.cdkBridgeInit) return;
            this.cdkBridgeInit = true;
            window.addEventListener('message', this.onCDKMessage);
            const iframe = document.createElement('iframe');
            iframe.id = 'lda-cdk-bridge';
            iframe.src = CONFIG.API.LINK_CDK;
            iframe.style.cssText = 'width:0;height:0;opacity:0;position:absolute;border:0;pointer-events:none;';
            document.body.appendChild(iframe);
            this.cdkBridgeFrame = iframe;
        }

        fetchCDKViaBridge() {
            return new Promise((resolve, reject) => {
                this.ensureCDKBridge();
                const timer = setTimeout(() => {
                    this.cdkWaiters = this.cdkWaiters.filter(fn => fn !== done);
                    reject(new Error('cdk bridge timeout'));
                }, 5000);
                const done = (data) => {
                    clearTimeout(timer);
                    resolve(data);
                };
                this.cdkWaiters.push(done);
                try {
                    this.cdkBridgeFrame?.contentWindow?.postMessage({ type: 'lda-cdk-request' }, CDK_BRIDGE_ORIGIN);
                } catch (_) { }
            });
        }

        onCDKMessage(event) {
            if (event.origin !== CDK_BRIDGE_ORIGIN) return;
            const payload = event.data?.payload || event.data;
            if (!payload?.data) return;
            this.cacheCDKData(payload.data);
            const waiters = [...this.cdkWaiters];
            this.cdkWaiters = [];
            waiters.forEach(fn => fn(payload.data));
        }

        cacheCDKData(data) {
            this.cdkCache = { data, ts: Date.now() };
            Utils.set(CONFIG.KEYS.CACHE_CDK, this.cdkCache);
        }

        isCDKCacheFresh() {
            return this.cdkCache && (Date.now() - (this.cdkCache.ts || 0) < CDK_CACHE_TTL);
        }

        renderCDKContent(info) {
            const wrap = this.dom.cdk;
            const trustLevelNames = {
                0: { zh: '新用户', en: 'New User' },
                1: { zh: '基本用户', en: 'Basic User' },
                2: { zh: '成员', en: 'Member' },
                3: { zh: '活跃用户', en: 'Regular' },
                4: { zh: '领导者', en: 'Leader' }
            };
            const trustName = trustLevelNames[info.trust_level]?.[this.state.lang] || `Lv.${info.trust_level}`;

            wrap.innerHTML = Utils.html`
                <div class="lda-card">
                    <div class="lda-actions-group">
                        <a href="${CONFIG.API.LINK_CDK}" target="_blank" class="lda-act-btn" title="${this.t('link_tip')}" id="btn-go-cdk-icon">
                            <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
                        </a>
                        <div class="lda-act-btn" id="btn-re-cdk" title="${this.t('refresh_tip_btn')}">
                            <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/></svg>
                        </div>
                    </div>
                    <div class="lda-credit-hero">
                        <div class="lda-credit-num" style="color:var(--lda-accent)">${info.score}</div>
                        <div class="lda-credit-label">${this.t('cdk_score')}</div>
                        <div style="margin-top:4px;font-size:11px;color:var(--lda-dim)">${this.t('cdk_score_desc')}</div>
                    </div>
                </div>
                <div class="lda-card">
                    <div style="font-size:11px;font-weight:700;color:var(--lda-dim);margin-bottom:10px;">用户信息</div>
                    <div class="lda-row-rec">
                        <span>${this.t('cdk_username')}</span>
                        <span class="lda-amt" style="color:var(--lda-fg)">${info.username}</span>
                    </div>
                    <div class="lda-row-rec">
                        <span>${this.t('cdk_nickname')}</span>
                        <span class="lda-amt" style="color:var(--lda-fg)">${info.nickname || '-'}</span>
                    </div>
                    <div class="lda-row-rec">
                        <span>${this.t('cdk_trust_level')}</span>
                        <span class="lda-amt" style="color:var(--lda-accent)">${trustName}</span>
                    </div>
                </div>
            `;
            Utils.el('#btn-re-cdk', wrap).onclick = (e) => { e.stopPropagation(); this.refreshCDK({ manual: true, force: true }); };

            const goIcon = Utils.el('#btn-go-cdk-icon', wrap);
            if (goIcon) goIcon.onclick = () => { this.focusFlags.cdk = true; };

            if (this.refreshingPages.cdk || this.refreshStopPending.cdk) {
                this.setRefreshBtnLoading('cdk', true);
            }
        }

        renderCDKAuth() {
            this.focusFlags.cdk = true;
            const wrap = this.dom.cdk;
            wrap.innerHTML = `
                <div class="lda-card lda-auth-card">
                    <div class="lda-auth-icon">
                        <svg viewBox="0 0 24 24" width="48" height="48"><path fill="currentColor" d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M17.13,17C15.92,18.85 14.11,20.24 12,20.92C9.89,20.24 8.08,18.85 6.87,17C6.53,16.5 6.24,16 6,15.47C6,13.82 8.71,12.47 12,12.47C15.29,12.47 18,13.79 18,15.47C17.76,16 17.47,16.5 17.13,17Z"/></svg>
                    </div>
                    <div class="lda-auth-title">${this.t('cdk_not_auth')}</div>
                    <div class="lda-auth-tip">${this.t('cdk_auth_tip')}</div>
                    <div class="lda-auth-btns">
                        <a href="${CONFIG.API.LINK_CDK}" target="_blank" class="lda-auth-btn" id="btn-go-cdk">${this.t('cdk_go_auth')} →</a>
                        <button id="btn-cdk-refresh" class="lda-auth-btn secondary">${this.t('cdk_refresh')}</button>
                    </div>
                </div>
            `;
            Utils.el('#btn-cdk-refresh', wrap).onclick = (e) => { e.stopPropagation(); this.refreshCDK({ manual: true, force: true }); };
            const go = Utils.el('#btn-go-cdk', wrap);
            if (go) go.onclick = () => { this.focusFlags.cdk = true; };
        }

        togglePanel(show) {
            this.dom.ball.style.display = show ? 'none' : 'flex';
            this.dom.panel.style.display = show ? 'flex' : 'none';
            if (show) {
                this.renderFromCacheAll();
                const needTrust = !this.trustData || this.isExpired('trust');
                const needCredit = !this.creditData || this.isExpired('credit');
                const needCDK = !this.cdkCache || this.isExpired('cdk');
                if (!this.dom.panel.dataset.loaded || needTrust || needCredit || needCDK) {
                    this.refreshTrust({ force: needTrust });
                    this.refreshCredit({ force: needCredit });
                    this.refreshCDK({ force: needCDK });
                    this.dom.panel.dataset.loaded = '1';
                }
                this.refreshSlowTipForPage(this.activePage);
            }
            if (show) this.updatePanelSide();
        }

        updatePanelSide() {
            const rect = this.dom.root.getBoundingClientRect();
            const rootWidth = rect.width || (this.dom.ball?.getBoundingClientRect().width) || 40;
            const panelWidth = this.dom.panel.getBoundingClientRect().width || 340;
            const spaceLeft = rect.left;
            const spaceRight = window.innerWidth - rect.right;

            let side = 'left';
            if (spaceRight >= panelWidth + 12) side = 'right';
            else if (spaceLeft >= panelWidth + 12) side = 'left';
            else side = spaceRight >= spaceLeft ? 'right' : 'left';

            this.dom.root.classList.toggle('lda-side-right', side === 'right');
            this.dom.root.classList.toggle('lda-side-left', side === 'left');

            const clampedLeft = Math.min(Math.max(rect.left, 0), Math.max(0, window.innerWidth - rootWidth));
            const clampedTop = Math.min(Math.max(rect.top, 0), Math.max(0, window.innerHeight - 50));
            this.dom.root.style.right = Math.max(0, window.innerWidth - clampedLeft - rootWidth) + 'px';
            this.dom.root.style.top = clampedTop + 'px';
        }

        applyTheme() {
            const { theme } = this.state;
            let isDark = (theme === 'dark');
            if (theme === 'auto') isDark = window.matchMedia('(prefers-color-scheme: dark)').matches || document.documentElement.className.includes('dark');
            this.dom.root.classList.toggle('lda-dark', isDark);
        }

        updateThemeIcon() {
            const icons = {
                light: '<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 00-1.41 0 .996.996 0 000 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 00-1.41 0 .996.996 0 000 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 000-1.41.996.996 0 00-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41a.996.996 0 00-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/></svg>',
                dark: '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/></svg>',
                auto: '<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M20 18c1.1 0 1.99-.9 1.99-2L22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z"/></svg>'
            };
            this.dom.themeBtn.innerHTML = icons[this.state.theme];
        }

        applyHeight() {
            this.dom.panel.className = `lda-panel h-${this.state.height}`;
        }

        applyOpacity() {
            const val = Math.max(0.5, Math.min(1, Number(this.state.opacity) || 1));
            this.state.opacity = val;
            if (this.dom.root) this.dom.root.style.setProperty('--lda-opacity', val);
        }

        initDrag() {
            let isDrag = false, hasDragged = false, startX, startY, startR, startT;

            const onMove = (e) => {
                if (!isDrag) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                if (Math.abs(dx) > 5 || Math.abs(dy) > 5) hasDragged = true;
                requestAnimationFrame(() => {
                    this.dom.root.style.right = Math.max(0, startR - dx) + 'px';
                    this.dom.root.style.top = Math.max(0, Math.min(startT + dy, window.innerHeight - 50)) + 'px';
                });
            };

            const onUp = () => {
                if (isDrag) {
                    isDrag = false;
                    this.dom.ball.classList.remove('dragging');
                    document.removeEventListener('mousemove', onMove);
                    document.removeEventListener('mouseup', onUp);
                    const r = this.dom.root.getBoundingClientRect();
                    Utils.set(CONFIG.KEYS.POS, { r: window.innerWidth - r.right, t: r.top });
                    this.updatePanelSide();
                }
            };

            const startDrag = (e, target) => {
                if (e.button !== 0) return;
                if (target === this.dom.head && e.target.closest('.lda-icon-btn')) return;
                isDrag = true;
                hasDragged = false;
                startX = e.clientX;
                startY = e.clientY;
                const rect = this.dom.root.getBoundingClientRect();
                startR = window.innerWidth - rect.right;
                startT = rect.top;
                if (target === this.dom.ball) this.dom.ball.classList.add('dragging');
                document.addEventListener('mousemove', onMove);
                document.addEventListener('mouseup', onUp);
                e.preventDefault();
            };

            this.dom.ball.onmousedown = (e) => startDrag(e, this.dom.ball);

            this.dom.ball.onclick = (e) => {
                if (hasDragged) {
                    hasDragged = false;
                    e.stopPropagation();
                    return;
                }
                this.togglePanel(true);
            };

            this.dom.head.onmousedown = (e) => startDrag(e, this.dom.head);
        }

        restorePos() {
            const p = Utils.get(CONFIG.KEYS.POS, { r: 20, t: 100 });
            this.dom.root.style.right = p.r + 'px';
            this.dom.root.style.top = p.t + 'px';
        }

        async checkUpdate(options = {}) {
            const { isAuto = false, force = false } = options;
            const btn = Utils.el('#lda-btn-update', this.dom.root);
            const updateUrl = 'https://raw.githubusercontent.com/dongshuyan/Linuxdo-Assistant/main/Linuxdo-Assistant.user.js';
            const now = Date.now();
            const ONE_HOUR = 60 * 60 * 1000;

            if (isAuto && !force) {
                if (now - (this.lastSkipUpdate || 0) < ONE_HOUR) return;
            }

            if (btn?.classList.contains('lda-cloud-pulse')) return;
            btn?.classList.add('lda-cloud-pulse');

            // 显示检查提示(1秒后淡出)
            if (!isAuto) {
                this.showToast(this.t('checking'), 'info', 1000);
            }

            try {
                const res = await Utils.request(updateUrl);
                const match = res.match(/@version\s+([\d.]+)/);
                if (!match) throw new Error('Parse error');

                const remote = match[1];
                const current = GM_info.script.version;

                if (this.compareVersion(remote, current) > 0) {
                    this.showUpdatePrompt(remote, updateUrl);
                } else if (!isAuto) {
                    this.showToast(`✓ ${this.t('latest')} (v${current})`, 'success');
                }
            } catch (e) {
                if (!isAuto) this.showToast(this.t('update_err'), 'error');
            }

            btn?.classList.remove('lda-cloud-pulse');
            Utils.set(CONFIG.KEYS.LAST_AUTO_CHECK, now);
            this.lastAutoCheck = now;
        }

        showToast(msg, type = 'info', duration = 2500) {
            const host = this.dom?.panel || document.body;
            const toast = document.createElement('div');
            toast.style.cssText = `
                position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
                padding: 10px 16px; border-radius: 8px; font-size: 13px; z-index: 1000000;
                background: ${type === 'success' ? 'var(--lda-green)' : type === 'error' ? 'var(--lda-red)' : 'var(--lda-accent)'};
                color: #fff; box-shadow: 0 4px 12px rgba(0,0,0,0.2); pointer-events:none;
                animation: lda-fade 0.2s; white-space: nowrap;
                transition: opacity 0.3s ease;
            `;
            toast.textContent = msg;
            host.appendChild(toast);
            setTimeout(() => {
                toast.style.opacity = '0';
                setTimeout(() => toast.remove(), 300);
            }, duration);
        }

        showUpdatePrompt(version, url) {
            const host = this.dom?.panel || document.body;
            const existing = Utils.el('#lda-update-mask', host);
            if (existing) existing.remove();
            const mask = document.createElement('div');
            mask.id = 'lda-update-mask';
            mask.style.cssText = `
                position:absolute; inset:0; background:rgba(0,0,0,0.12); z-index:1000001;
                display:flex; align-items:center; justify-content:center;
            `;
            const box = document.createElement('div');
            box.style.cssText = `
                background:var(--lda-bg); color:var(--lda-fg); border:1px solid var(--lda-border);
                border-radius:12px; padding:16px 18px; min-width:260px; box-shadow:0 10px 30px rgba(0,0,0,0.25);
            `;
            box.innerHTML = `
                <div style="font-size:14px;font-weight:700;margin-bottom:8px;color:var(--lda-accent);">发现新版本 v${version}</div>
                <div style="font-size:12px;color:var(--lda-dim);margin-bottom:14px;">是否更新到最新版本?</div>
                <div style="display:flex; gap:8px; justify-content:flex-end;">
                    <button id="lda-update-skip" style="padding:8px 12px; border:1px solid var(--lda-border); background:var(--lda-bg); border-radius:8px; cursor:pointer;">暂不更新</button>
                    <button id="lda-update-go" style="padding:8px 12px; border:none; background:var(--lda-accent); color:#fff; border-radius:8px; cursor:pointer;">立即更新</button>
                </div>
            `;
            mask.appendChild(box);
            host.appendChild(mask);
            const dispose = () => mask.remove();
            box.querySelector('#lda-update-go').onclick = (e) => {
                e.stopPropagation();
                try { window.open(url, '_blank'); } catch (_) { }
                dispose();
            };
            box.querySelector('#lda-update-skip').onclick = (e) => {
                e.stopPropagation();
                const now = Date.now();
                this.lastSkipUpdate = now;
                Utils.set(CONFIG.KEYS.LAST_SKIP_UPDATE, now);
                dispose();
            };
            mask.onclick = dispose;
            box.onclick = (e) => e.stopPropagation();
        }

        compareVersion(v1, v2) {
            const a = v1.split('.').map(Number);
            const b = v2.split('.').map(Number);
            for (let i = 0; i < Math.max(a.length, b.length); i++) {
                const n1 = a[i] || 0, n2 = b[i] || 0;
                if (n1 > n2) return 1;
                if (n1 < n2) return -1;
            }
            return 0;
        }
    }

    new App().init();
})();