Telegram Ultimate Enhancer

Telegram Web 低开销增强:恢复有充分证据的原生下载/转发动作,解除受保护内容复制与右键限制,并提供剧透解除、倍速、画中画、外链净化和防误拨。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Telegram Ultimate Enhancer
// @name:zh-CN   Telegram 终极增强器
// @version      1.0.0
// @namespace    https://github.com/xiaohuitongxue88-ctrl/Telegram-Ultimate-Enhancer
// @description  Telegram Web 低开销增强:恢复有充分证据的原生下载/转发动作,解除受保护内容复制与右键限制,并提供剧透解除、倍速、画中画、外链净化和防误拨。
// @description:zh-CN Telegram Web 低开销增强:恢复有充分证据的原生下载/转发动作,解除受保护内容复制与右键限制,并提供剧透解除、倍速、画中画、外链净化和防误拨。
// @author       xiaohuitongxue
// @license      GNU GPLv3
// @homepageURL  https://github.com/xiaohuitongxue88-ctrl/Telegram-Ultimate-Enhancer
// @supportURL   https://github.com/xiaohuitongxue88-ctrl/Telegram-Ultimate-Enhancer/issues
// @match        https://web.telegram.org/*
// @match        https://webk.telegram.org/*
// @match        https://webz.telegram.org/*
// @icon         https://img.icons8.com/color/452/telegram-app--v5.png
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==

/*
 * Telegram Ultimate Enhancer
 * Copyright (C) 2026 xiaohuitongxue
 * Licensed under GNU GPLv3.
 * Additional terms under GPLv3 Section 7 for project-authored material:
 * see https://github.com/xiaohuitongxue88-ctrl/Telegram-Ultimate-Enhancer/blob/main/ADDITIONAL_TERMS.md
 */

(function () {
    'use strict';

    // =============================================================================
    // 01. 核心配置
    // =============================================================================
    const Config = Object.freeze({
        VERSION: '1.0.0',
        DEBUG: false,

        HIDE_PINNED_MESSAGES: true,
        HIDE_SPONSORED_MESSAGES: true,
        UNLOCK_SPOILERS: true,
        UNLOCK_PROTECTED_COPY: true,
        OPEN_EXTERNAL_LINKS_IN_NEW_TAB: true,
        CLEAN_TRACKING_PARAMS_ON_COPY: true,
        CONFIRM_CALL_ACTION: true,

        FEEDBACK_PROMPT_ENABLED: true,
        FEEDBACK_TEST_MODE: false,
        FEEDBACK_TEST_DELAY_MS: 1500,
        FEEDBACK_IDLE_DELAY_MS: 10000,
        FEEDBACK_SESSION_GAP_MS: 30 * 60 * 1000,

        FEEDBACK_URL:
            'https://greasyfork.org/zh-CN/scripts/590834-telegram-ultimate-enhancer/feedback',
        GITHUB_URL:
            'https://github.com/xiaohuitongxue88-ctrl/Telegram-Ultimate-Enhancer',

        FEEDBACK_STAGES: Object.freeze([
            Object.freeze({
                minAgeMs: 3 * 24 * 60 * 60 * 1000,
                minSessions: 3,
                minSuccess: 5,
                minFeatures: 2,
                minGapMs: 0
            }),
            Object.freeze({
                minAgeMs: 14 * 24 * 60 * 60 * 1000,
                minSessions: 10,
                minSuccess: 25,
                minFeatures: 3,
                minGapMs: 10 * 24 * 60 * 60 * 1000
            }),
            Object.freeze({
                minAgeMs: 45 * 24 * 60 * 60 * 1000,
                minSessions: 25,
                minSuccess: 80,
                minFeatures: 3,
                minGapMs: 30 * 24 * 60 * 60 * 1000
            })
        ]),

        TRACKERS: Object.freeze([
            'utm_source',
            'utm_medium',
            'utm_campaign',
            'utm_term',
            'utm_content',
            'si',
            'igshid',
            'fbclid',
            'gclid',
            'share_id'
        ]),

        DOM_FLUSH_DELAY_MS: 28,
        DOM_MAX_ROOTS_PER_FLUSH: 24,
        DOM_QUEUE_SOFT_LIMIT: 96,

        TOAST_DURATION: 1600
    });

    // =============================================================================
    // 02. 日志
    // =============================================================================
    const Log = {
        info(tag, ...args) {
            if (Config.DEBUG) {
                console.log(
                    `[Telegram Ultimate Enhancer V${Config.VERSION}] [${tag}]`,
                    ...args
                );
            }
        },

        warn(tag, ...args) {
            if (Config.DEBUG) {
                console.warn(
                    `[Telegram Ultimate Enhancer V${Config.VERSION}] [${tag}]`,
                    ...args
                );
            }
        },

        error(tag, ...args) {
            console.error(
                `[Telegram Ultimate Enhancer V${Config.VERSION}] [${tag}]`,
                ...args
            );
        }
    };

    // =============================================================================
    // 03. 弱引用缓存
    // =============================================================================
    const Cache = {
        processedButtons: new WeakSet()
    };

    // =============================================================================
    // 04. 通用工具
    // =============================================================================
    const Utils = {
        isEditableTarget(target) {
            if (!(target instanceof Element)) return false;
            if (target.isContentEditable) return true;

            const tag = target.tagName;
            if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
                return true;
            }

            return Boolean(
                target.closest(
                    'input,textarea,select,[contenteditable="true"],[role="textbox"]'
                )
            );
        },

        getActiveVideo() {
            const viewerVideo = document.querySelector(
                '.media-viewer-aspecter video,.media-viewer video'
            );
            if (viewerVideo) return viewerVideo;

            const videos = Array.from(document.querySelectorAll('video'));
            if (!videos.length) return null;

            let best = null;
            let bestVisibleArea = 0;

            for (const video of videos) {
                const rect = video.getBoundingClientRect();
                if (rect.width <= 1 || rect.height <= 1) continue;

                const style = getComputedStyle(video);
                if (
                    style.display === 'none' ||
                    style.visibility === 'hidden' ||
                    Number(style.opacity || 1) <= 0
                ) {
                    continue;
                }

                const vw =
                    window.innerWidth || document.documentElement.clientWidth;
                const vh =
                    window.innerHeight || document.documentElement.clientHeight;

                const visibleWidth = Math.max(
                    0,
                    Math.min(rect.right, vw) - Math.max(rect.left, 0)
                );
                const visibleHeight = Math.max(
                    0,
                    Math.min(rect.bottom, vh) - Math.max(rect.top, 0)
                );
                const area = visibleWidth * visibleHeight;

                if (area > bestVisibleArea) {
                    best = video;
                    bestVisibleArea = area;
                }
            }

            return best || videos[0];
        },

        selectionText() {
            try {
                return window.getSelection()?.toString().trim() || '';
            } catch {
                return '';
            }
        },

        cleanUrlTracking(rawUrl) {
            try {
                const url = new URL(rawUrl);
                let modified = false;

                for (const key of Config.TRACKERS) {
                    if (url.searchParams.has(key)) {
                        url.searchParams.delete(key);
                        modified = true;
                    }
                }

                return { modified, url: url.toString() };
            } catch {
                return { modified: false, url: rawUrl };
            }
        },

        isExternalHttpUrl(rawUrl) {
            try {
                const url = new URL(rawUrl, location.href);

                if (url.protocol !== 'http:' && url.protocol !== 'https:') {
                    return false;
                }

                const host = url.hostname.toLowerCase();

                return !(
                    host === 'telegram.org' ||
                    host.endsWith('.telegram.org') ||
                    host === 't.me' ||
                    host.endsWith('.t.me')
                );
            } catch {
                return false;
            }
        },

        openNewTab(url) {
            try {
                return window.open(
                    String(url || ''),
                    '_blank',
                    'noopener,noreferrer'
                );
            } catch (error) {
                Log.warn('OPEN_TAB', error);
                return null;
            }
        }
    };

    // =============================================================================
    // 05. GPU Safe 单实例 Toast
    // =============================================================================
    const UI = {
        toast: null,
        toastTimer: 0,

        initToast() {
            if (this.toast || !document.body) return;

            const toast = document.createElement('div');
            toast.id = 'telegram-ultimate-enhancer-toast';
            toast.setAttribute('role', 'status');
            toast.setAttribute('aria-live', 'polite');
            document.body.appendChild(toast);

            this.toast = toast;
        },

        showToast(message, duration = Config.TOAST_DURATION) {
            if (!document.body) return;

            this.initToast();
            if (!this.toast) return;

            clearTimeout(this.toastTimer);

            this.toast.textContent = String(message || '');
            this.toast.style.display = 'block';

            if (duration > 0) {
                this.toastTimer = setTimeout(() => {
                    if (this.toast) {
                        this.toast.style.display = 'none';
                    }
                }, duration);
            }
        }
    };

    // =============================================================================
    // 06. 使用反馈提示
    // =============================================================================
    const FeedbackPrompt = {
        STORAGE_KEY: 'telegram-ultimate-enhancer.feedback.v1',

        panel: null,
        eligibilityTimer: 0,
        testTimer: 0,
        passiveFeatureTimes: new Map(),

        defaultState() {
            return {
                firstSeenAt: Date.now(),
                lastSessionAt: 0,
                sessionCount: 0,
                successCount: 0,
                featureFlags: [],
                promptCount: 0,
                lastPromptAt: 0,
                actionClicked: false
            };
        },

        loadState() {
            const fallback = this.defaultState();

            try {
                const raw = localStorage.getItem(this.STORAGE_KEY);
                if (!raw) {
                    this.saveState(fallback);
                    return fallback;
                }

                const parsed = JSON.parse(raw);
                const state = {
                    ...fallback,
                    ...parsed
                };

                if (!Array.isArray(state.featureFlags)) {
                    state.featureFlags = [];
                }

                return state;
            } catch (error) {
                Log.warn('FEEDBACK_STORAGE_READ', error);
                return fallback;
            }
        },

        saveState(state) {
            try {
                localStorage.setItem(
                    this.STORAGE_KEY,
                    JSON.stringify(state)
                );
                return true;
            } catch (error) {
                Log.warn('FEEDBACK_STORAGE_WRITE', error);
                return false;
            }
        },

        init() {
            if (!Config.FEEDBACK_PROMPT_ENABLED || !document.body) return;

            if (Config.FEEDBACK_TEST_MODE) {
                clearTimeout(this.testTimer);

                this.testTimer = setTimeout(() => {
                    this.show({ testMode: true });
                }, Config.FEEDBACK_TEST_DELAY_MS);

                return;
            }

            this.loadState();
        },

        recordSuccess(feature, options = {}) {
            if (!Config.FEEDBACK_PROMPT_ENABLED) return;
            if (Config.FEEDBACK_TEST_MODE) return;

            const featureName = String(feature || '').trim();
            if (!featureName) return;

            const now = Date.now();
            const cooldownMs = Math.max(
                0,
                Number(options.cooldownMs || 0)
            );

            if (cooldownMs > 0) {
                const last = Number(
                    this.passiveFeatureTimes.get(featureName) || 0
                );

                if (now - last < cooldownMs) {
                    return;
                }

                this.passiveFeatureTimes.set(featureName, now);
            }

            const state = this.loadState();
            if (state.actionClicked) return;

            state.successCount =
                Math.max(0, Number(state.successCount || 0)) + 1;

            const features = new Set(state.featureFlags || []);
            features.add(featureName);
            state.featureFlags = Array.from(features);

            const lastSessionAt = Number(state.lastSessionAt || 0);
            if (
                !lastSessionAt ||
                now - lastSessionAt >= Config.FEEDBACK_SESSION_GAP_MS
            ) {
                state.sessionCount =
                    Math.max(0, Number(state.sessionCount || 0)) + 1;
                state.lastSessionAt = now;
            }

            this.saveState(state);
            this.scheduleIfEligible(state);
        },

        getCurrentStage(state) {
            const index = Math.max(
                0,
                Number(state.promptCount || 0)
            );

            return Config.FEEDBACK_STAGES[index] || null;
        },

        isEligible(state) {
            if (!state || state.actionClicked) return false;

            const stage = this.getCurrentStage(state);
            if (!stage) return false;

            const now = Date.now();
            const firstSeenAt = Number(state.firstSeenAt || now);
            const lastPromptAt = Number(state.lastPromptAt || 0);
            const featureCount = new Set(
                state.featureFlags || []
            ).size;

            if (now - firstSeenAt < stage.minAgeMs) return false;
            if (Number(state.sessionCount || 0) < stage.minSessions) {
                return false;
            }
            if (Number(state.successCount || 0) < stage.minSuccess) {
                return false;
            }
            if (featureCount < stage.minFeatures) return false;

            if (
                stage.minGapMs > 0 &&
                lastPromptAt > 0 &&
                now - lastPromptAt < stage.minGapMs
            ) {
                return false;
            }

            return true;
        },

        isSafeMoment() {
            if (!document.body) return false;
            if (document.hidden) return false;
            if (document.fullscreenElement) return false;

            const active = document.activeElement;
            if (
                active instanceof Element &&
                Utils.isEditableTarget(active)
            ) {
                return false;
            }

            return true;
        },

        scheduleIfEligible(state) {
            if (!this.isEligible(state)) return;
            if (this.panel || this.eligibilityTimer) return;

            this.eligibilityTimer = setTimeout(() => {
                this.eligibilityTimer = 0;

                const fresh = this.loadState();
                if (!this.isEligible(fresh)) return;
                if (!this.isSafeMoment()) return;

                this.show({ testMode: false });
            }, Config.FEEDBACK_IDLE_DELAY_MS);
        },

        removePanel() {
            if (this.panel?.isConnected) {
                this.panel.remove();
            }

            this.panel = null;
        },

        markActionClicked() {
            if (Config.FEEDBACK_TEST_MODE) return;

            const state = this.loadState();
            state.actionClicked = true;
            this.saveState(state);
        },

        show({ testMode = false } = {}) {
            if (!document.body || this.panel) return;

            if (!testMode) {
                const state = this.loadState();

                if (!this.isEligible(state)) {
                    return;
                }

                state.promptCount =
                    Math.max(0, Number(state.promptCount || 0)) + 1;
                state.lastPromptAt = Date.now();
                this.saveState(state);
            }

            const panel = document.createElement('section');
            panel.id = 'tue-feedback-prompt';
            panel.setAttribute('role', 'region');
            panel.setAttribute('aria-label', '支持项目持续维护');

            panel.innerHTML = `
                <div class="tue-feedback-head">
                    <div class="tue-feedback-title">
                        支持项目持续维护
                    </div>

                    <button
                        type="button"
                        class="tue-feedback-close"
                        aria-label="关闭本次提示"
                        title="关闭"
                    >×</button>
                </div>

                <div class="tue-feedback-copy">
                    <p>
                        如果 <strong>Telegram Ultimate Enhancer</strong>
                        对你的实际使用有所帮助,欢迎留下真实评价或为项目添加 GitHub Star。
                    </p>

                    <p>
                        <strong>
                            你的好评、Star 和推荐,是我持续更新、修复兼容问题和开发新功能的重要动力。
                        </strong>
                    </p>

                    <p>
                        长期缺少真实反馈和社区支持,也意味着项目的持续需求有限,
                        后续维护和新功能开发可能相应减少。
                    </p>
                </div>

                <div class="tue-feedback-actions">
                    <button
                        type="button"
                        class="tue-feedback-action tue-feedback-primary"
                        data-tue-feedback-action="greasyfork"
                    >
                        提交 Greasy Fork 评价
                    </button>

                    <button
                        type="button"
                        class="tue-feedback-action tue-feedback-secondary"
                        data-tue-feedback-action="github"
                    >
                        GitHub Star
                    </button>
                </div>
            `;

            const closeButton = panel.querySelector(
                '.tue-feedback-close'
            );

            closeButton?.addEventListener(
                'click',
                () => this.removePanel(),
                { once: true }
            );

            panel
                .querySelector(
                    '[data-tue-feedback-action="greasyfork"]'
                )
                ?.addEventListener(
                    'click',
                    () => {
                        this.markActionClicked();
                        Utils.openNewTab(Config.FEEDBACK_URL);
                        this.removePanel();
                    },
                    { once: true }
                );

            panel
                .querySelector(
                    '[data-tue-feedback-action="github"]'
                )
                ?.addEventListener(
                    'click',
                    () => {
                        this.markActionClicked();
                        Utils.openNewTab(Config.GITHUB_URL);
                        this.removePanel();
                    },
                    { once: true }
                );

            document.body.appendChild(panel);
            this.panel = panel;
        }
    };

    // =============================================================================
    // 07. Telegram 原生动作证据引擎
    // =============================================================================
    const NativeActionEvidence = Object.freeze({
        ACTION_TERMS: Object.freeze({
            download: Object.freeze(['download', '下载', 'save', '保存']),
            forward: Object.freeze(['forward', '转发'])
        }),

        GLYPH_HINTS: Object.freeze({
            '\ue977': 'download',
            '\ue995': 'forward'
        })
    });

    const ContextDetector = {
        CONTEXT_SELECTOR: [
            '.media-viewer-topbar',
            '.media-viewer-buttons',
            '#MediaViewer .MediaViewerActions',
            '#MediaViewer [class*="MediaViewerActions"]'
        ].join(','),

        isActionContext(element) {
            if (!(element instanceof Element)) return false;

            try {
                return element.matches(this.CONTEXT_SELECTOR);
            } catch {
                return false;
            }
        },

        closestActionContext(element) {
            if (!(element instanceof Element)) return null;

            try {
                if (this.isActionContext(element)) return element;
                return element.closest(this.CONTEXT_SELECTOR);
            } catch {
                return null;
            }
        },

        collectContexts(root) {
            if (!(root instanceof Element)) return [];

            const contexts = new Set();
            const direct = this.closestActionContext(root);
            if (direct) contexts.add(direct);

            try {
                for (
                    const context of root.querySelectorAll(
                        this.CONTEXT_SELECTOR
                    )
                ) {
                    contexts.add(context);
                }
            } catch {}

            return Array.from(contexts);
        }
    };

    const CandidateCollector = {
        CANDIDATE_SELECTOR: [
            'button',
            '[role="button"]',
            '.btn-icon',
            '.tgico-download',
            '.tgico-forward'
        ].join(','),

        collect(root) {
            if (!(root instanceof Element)) return [];

            const candidates = new Set();
            const contexts = ContextDetector.collectContexts(root);

            for (const context of contexts) {
                try {
                    if (context.matches(this.CANDIDATE_SELECTOR)) {
                        candidates.add(context);
                    }

                    for (
                        const element of context.querySelectorAll(
                            this.CANDIDATE_SELECTOR
                        )
                    ) {
                        candidates.add(element);
                    }
                } catch {}
            }

            return Array.from(candidates);
        }
    };

    const ActionClassifier = {
        normalize(value) {
            return String(value || '')
                .toLowerCase()
                .replace(/[_-]+/g, ' ')
                .replace(/\s+/g, ' ')
                .trim();
        },

        findAction(value) {
            const text = this.normalize(value);
            if (!text) return 'unknown';

            for (
                const [action, terms]
                of Object.entries(NativeActionEvidence.ACTION_TERMS)
            ) {
                if (terms.some(term => text.includes(term))) {
                    return action;
                }
            }

            return 'unknown';
        },

        classify(element) {
            const evidence = {
                action: 'unknown',
                direct: [],
                strong: [],
                weak: []
            };

            if (!(element instanceof Element)) return evidence;

            const actionVotes = [];
            const record = (
                bucket,
                source,
                value,
                action
            ) => {
                if (action === 'unknown') return;
                bucket.push(`${source}:${value}`);
                actionVotes.push(action);
            };

            const directSources = [
                ['aria-label', element.getAttribute('aria-label')],
                ['title', element.getAttribute('title')],
                ['data-action', element.getAttribute('data-action')],
                ['data-testid', element.getAttribute('data-testid')]
            ];

            for (const [source, value] of directSources) {
                const action = this.findAction(value);
                record(
                    evidence.direct,
                    source,
                    this.normalize(value),
                    action
                );
            }

            const className = this.normalize(element.className);
            const classAction = this.findAction(className);
            record(
                evidence.strong,
                'semantic-class',
                className,
                classAction
            );

            const context =
                ContextDetector.closestActionContext(element);
            if (context) {
                evidence.strong.push(
                    'telegram-media-action-context'
                );
            }

            if (
                element.matches?.(
                    'button,.btn-icon,[role="button"]'
                ) &&
                (
                    className.includes('btn icon') ||
                    className.includes('btn-icon') ||
                    element.tagName === 'BUTTON'
                )
            ) {
                evidence.strong.push(
                    'telegram-native-button-shape'
                );
            }

            const glyph =
                String(element.textContent || '').trim();
            const glyphAction =
                NativeActionEvidence.GLYPH_HINTS[glyph] ||
                'unknown';

            record(
                evidence.weak,
                'glyph-hint',
                glyph
                    ? `U+${glyph.codePointAt(0).toString(16)}`
                    : '',
                glyphAction
            );

            const uniqueVotes =
                Array.from(new Set(actionVotes));

            if (uniqueVotes.length === 1) {
                evidence.action = uniqueVotes[0];
            } else if (uniqueVotes.length > 1) {
                evidence.action = 'unknown';
                evidence.weak.push(
                    `conflict:${uniqueVotes.join('|')}`
                );
            }

            return evidence;
        }
    };

    const VisibilityInspector = {
        inspect(element) {
            const result = {
                hidden: false,
                reasons: []
            };

            if (!(element instanceof HTMLElement)) {
                return result;
            }

            if (element.classList.contains('hide')) {
                result.reasons.push('class:hide');
            }

            if (
                element.hidden ||
                element.hasAttribute('hidden')
            ) {
                result.reasons.push('attribute:hidden');
            }

            const inline = element.style;

            if (inline.display === 'none') {
                result.reasons.push('style:display-none');
            }
            if (inline.visibility === 'hidden') {
                result.reasons.push(
                    'style:visibility-hidden'
                );
            }
            if (inline.pointerEvents === 'none') {
                result.reasons.push(
                    'style:pointer-events-none'
                );
            }
            if (Number(inline.opacity || 1) <= 0) {
                result.reasons.push('style:opacity-zero');
            }

            result.hidden = result.reasons.length > 0;
            return result;
        }
    };

    const EvidenceGate = {
        allows(classification) {
            if (
                !classification ||
                classification.action === 'unknown'
            ) {
                return false;
            }

            const direct = classification.direct || [];
            const strong = classification.strong || [];

            return direct.length > 0 || strong.length >= 2;
        }
    };

    const ActionRestorer = {
        restore(element, classification, visibility) {
            if (!(element instanceof HTMLElement)) {
                return false;
            }
            if (!EvidenceGate.allows(classification)) {
                return false;
            }
            if (!visibility?.hidden) {
                return false;
            }

            let changed = false;

            if (element.classList.contains('hide')) {
                element.classList.remove('hide');
                changed = true;
            }

            if (element.hasAttribute('hidden')) {
                element.removeAttribute('hidden');
                changed = true;
            }

            if (element.style.display === 'none') {
                element.style.removeProperty('display');
                changed = true;
            }

            if (element.style.visibility === 'hidden') {
                element.style.removeProperty('visibility');
                changed = true;
            }

            if (element.style.pointerEvents === 'none') {
                element.style.removeProperty('pointer-events');
                changed = true;
            }

            if (Number(element.style.opacity || 1) <= 0) {
                element.style.removeProperty('opacity');
                changed = true;
            }

            if (changed) {
                element.classList.add(
                    'tue-native-action-restored'
                );
                element.dataset.tueAction =
                    classification.action;
                Cache.processedButtons.add(element);

                FeedbackPrompt.recordSuccess(
                    'native-action',
                    { cooldownMs: 60 * 1000 }
                );

                Log.info(
                    'NATIVE_ACTION',
                    `恢复 Telegram 原生 ${classification.action} 动作`,
                    {
                        direct: classification.direct,
                        strong: classification.strong,
                        weak: classification.weak,
                        hiddenReasons: visibility.reasons
                    }
                );
            }

            return changed;
        }
    };

    const NativeActionEngine = {
        mayContainCandidate(root) {
            if (!(root instanceof Element)) return false;

            try {
                if (
                    ContextDetector.closestActionContext(root)
                ) {
                    return true;
                }

                return Boolean(
                    root.querySelector(
                        ContextDetector.CONTEXT_SELECTOR
                    )
                );
            } catch {
                return false;
            }
        },

        processElement(element) {
            if (!(element instanceof HTMLElement)) {
                return false;
            }

            const visibility =
                VisibilityInspector.inspect(element);

            if (!visibility.hidden) return false;

            const classification =
                ActionClassifier.classify(element);

            return ActionRestorer.restore(
                element,
                classification,
                visibility
            );
        },

        processRoot(root) {
            if (!(root instanceof Element)) return 0;
            if (!this.mayContainCandidate(root)) return 0;

            let restored = 0;

            for (
                const candidate
                of CandidateCollector.collect(root)
            ) {
                if (this.processElement(candidate)) {
                    restored++;
                }
            }

            return restored;
        },

        initialScan() {
            if (!document.body) return 0;
            return this.processRoot(document.body);
        }
    };

    // =============================================================================
    // 08. 普通任务 DOM 调度器
    // =============================================================================
    const Scheduler = {
        pendingRoots: new Set(),
        flushTimer: 0,

        enqueue(root) {
            if (!(root instanceof Element)) return;

            if (!NativeActionEngine.mayContainCandidate(root)) {
                return;
            }

            for (const existing of this.pendingRoots) {
                if (
                    existing === root ||
                    existing.contains(root)
                ) {
                    return;
                }
            }

            for (
                const existing
                of Array.from(this.pendingRoots)
            ) {
                if (root.contains(existing)) {
                    this.pendingRoots.delete(existing);
                }
            }

            this.pendingRoots.add(root);

            if (
                this.pendingRoots.size >
                Config.DOM_QUEUE_SOFT_LIMIT
            ) {
                const oldest =
                    this.pendingRoots.values().next().value;

                if (oldest) {
                    this.pendingRoots.delete(oldest);
                }
            }

            this.schedule();
        },

        schedule() {
            if (this.flushTimer) return;

            this.flushTimer = setTimeout(
                () => this.flush(),
                Config.DOM_FLUSH_DELAY_MS
            );
        },

        flush() {
            this.flushTimer = 0;

            if (!this.pendingRoots.size) return;

            const roots = [];

            for (const root of this.pendingRoots) {
                roots.push(root);

                if (
                    roots.length >=
                    Config.DOM_MAX_ROOTS_PER_FLUSH
                ) {
                    break;
                }
            }

            for (const root of roots) {
                this.pendingRoots.delete(root);
                NativeActionEngine.processRoot(root);
            }

            if (this.pendingRoots.size) {
                this.schedule();
            }
        }
    };

    // =============================================================================
    // 09. 精准 MutationObserver
    // =============================================================================
    const ObserverEngine = {
        observer: null,

        start() {
            if (this.observer || !document.body) return;

            this.observer =
                new MutationObserver(mutations => {
                    for (const mutation of mutations) {
                        if (!mutation.addedNodes?.length) {
                            continue;
                        }

                        for (
                            const node
                            of mutation.addedNodes
                        ) {
                            if (node instanceof Element) {
                                Scheduler.enqueue(node);
                            }
                        }
                    }
                });

            this.observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            Log.info(
                'OBSERVER',
                'DOM 增量观察器已启动'
            );
        },

        stop() {
            if (!this.observer) return;

            this.observer.disconnect();
            this.observer = null;
        }
    };

    // =============================================================================
    // 10. 精准事件委托
    // =============================================================================
    const EventHub = {
        initialized: false,

        init() {
            if (this.initialized) return;
            this.initialized = true;

            this.setupCopy();
            this.setupContextMenu();
            this.setupKeyboard();
            this.setupClick();
        },

        setupKeyboard() {
            window.addEventListener(
                'keydown',
                event => {
                    if (
                        Utils.isEditableTarget(
                            event.target
                        )
                    ) {
                        return;
                    }

                    const key =
                        String(event.key || '')
                            .toLowerCase();

                    if (
                        key !== '[' &&
                        key !== ']' &&
                        key !== 'p'
                    ) {
                        return;
                    }

                    const video =
                        Utils.getActiveVideo();

                    if (!video) return;

                    if (key === ']') {
                        video.playbackRate =
                            Math.min(
                                4,
                                Number(
                                    video.playbackRate || 1
                                ) + 0.25
                            );

                        UI.showToast(
                            `倍速 ${video.playbackRate.toFixed(2)}×`
                        );

                        FeedbackPrompt.recordSuccess(
                            'speed'
                        );
                        return;
                    }

                    if (key === '[') {
                        video.playbackRate =
                            Math.max(
                                0.25,
                                Number(
                                    video.playbackRate || 1
                                ) - 0.25
                            );

                        UI.showToast(
                            `倍速 ${video.playbackRate.toFixed(2)}×`
                        );

                        FeedbackPrompt.recordSuccess(
                            'speed'
                        );
                        return;
                    }

                    if (key === 'p') {
                        if (
                            document.pictureInPictureElement
                        ) {
                            const result =
                                document
                                    .exitPictureInPicture?.();

                            if (
                                result &&
                                typeof result.then ===
                                    'function'
                            ) {
                                result
                                    .then(() => {
                                        FeedbackPrompt
                                            .recordSuccess(
                                                'pip'
                                            );
                                    })
                                    .catch(() =>
                                        UI.showToast(
                                            '退出画中画失败'
                                        )
                                    );
                            }
                        } else if (
                            typeof video
                                .requestPictureInPicture ===
                            'function'
                        ) {
                            video
                                .requestPictureInPicture()
                                .then(() => {
                                    FeedbackPrompt
                                        .recordSuccess(
                                            'pip'
                                        );
                                })
                                .catch(() =>
                                    UI.showToast(
                                        '画中画被浏览器拒绝'
                                    )
                                );
                        }
                    }
                },
                true
            );
        },

        setupCopy() {
            if (
                !Config.UNLOCK_PROTECTED_COPY &&
                !Config.CLEAN_TRACKING_PARAMS_ON_COPY
            ) {
                return;
            }

            window.addEventListener(
                'copy',
                event => {
                    if (
                        Utils.isEditableTarget(
                            event.target
                        )
                    ) {
                        return;
                    }

                    const selectedText =
                        Utils.selectionText();

                    if (!selectedText) return;

                    let outputText = selectedText;
                    let cleanedTracking = false;

                    if (
                        Config
                            .CLEAN_TRACKING_PARAMS_ON_COPY &&
                        /^https?:\/\/\S+$/i.test(
                            selectedText
                        )
                    ) {
                        const result =
                            Utils.cleanUrlTracking(
                                selectedText
                            );

                        if (result.modified) {
                            outputText = result.url;
                            cleanedTracking = true;
                        }
                    }

                    try {
                        if (event.clipboardData) {
                            event.clipboardData
                                .setData(
                                    'text/plain',
                                    outputText
                                );

                            event.preventDefault();
                            event.stopImmediatePropagation();

                            UI.showToast(
                                cleanedTracking
                                    ? '已复制,并清理链接跟踪参数'
                                    : '已复制选中文本'
                            );

                            FeedbackPrompt.recordSuccess(
                                cleanedTracking
                                    ? 'link-clean'
                                    : 'copy'
                            );
                            return;
                        }

                        if (
                            Config.UNLOCK_PROTECTED_COPY
                        ) {
                            event.stopImmediatePropagation();
                        }
                    } catch (error) {
                        Log.warn('COPY', error);
                    }
                },
                true
            );
        },

        setupContextMenu() {
            window.addEventListener(
                'contextmenu',
                event => {
                    const target = event.target;

                    if (!(target instanceof Element)) {
                        return;
                    }

                    if (
                        Config.UNLOCK_PROTECTED_COPY &&
                        Utils.selectionText()
                    ) {
                        event.stopImmediatePropagation();
                        return;
                    }

                    if (
                        target.closest(
                            'video,audio,img,.media-viewer,.media-container'
                        )
                    ) {
                        event.stopImmediatePropagation();
                    }
                },
                true
            );
        },

        setupClick() {
            window.addEventListener(
                'click',
                event => {
                    const target = event.target;

                    if (!(target instanceof Element)) {
                        return;
                    }

                    if (
                        Config.CONFIRM_CALL_ACTION
                    ) {
                        const callButton =
                            target.closest(
                                [
                                    '.tgico-phone',
                                    '.tgico-call',
                                    '[title*="Call" i]',
                                    '[title*="通话" i]',
                                    '[aria-label*="Call" i]'
                                ].join(',')
                            );

                        if (callButton) {
                            const confirmed =
                                window.confirm(
                                    '确定要发起语音/视频通话吗?'
                                );

                            if (!confirmed) {
                                event.preventDefault();
                                event.stopImmediatePropagation();

                                UI.showToast(
                                    '已取消通话操作'
                                );
                                return;
                            }
                        }
                    }

                    if (
                        !Config
                            .OPEN_EXTERNAL_LINKS_IN_NEW_TAB
                    ) {
                        return;
                    }

                    if (
                        event.button !== 0 ||
                        event.ctrlKey ||
                        event.shiftKey ||
                        event.altKey ||
                        event.metaKey
                    ) {
                        return;
                    }

                    const anchor =
                        target.closest('a[href]');

                    if (!anchor) return;
                    if (
                        anchor.hasAttribute('download')
                    ) {
                        return;
                    }
                    if (anchor.target === '_blank') {
                        return;
                    }

                    const href = anchor.href;

                    if (
                        !href ||
                        !Utils.isExternalHttpUrl(href)
                    ) {
                        return;
                    }

                    event.preventDefault();
                    event.stopPropagation();

                    Utils.openNewTab(href);
                    FeedbackPrompt.recordSuccess(
                        'external-link'
                    );
                },
                true
            );
        }
    };

    // =============================================================================
    // 11. 静态 CSS
    // =============================================================================
    function injectStaticStyle() {
        const rules = [];

        rules.push(`
            .message-text,
            .text-content,
            .translatable-message,
            .message .text,
            .bubble .text,
            .bubble-content .text,
            .is-protected,
            .restricted-content,
            .nocopy {
                -webkit-user-select: text !important;
                user-select: text !important;
            }

            .text-content,
            .message-text,
            .translatable-message {
                pointer-events: auto !important;
            }

            .is-protected::after,
            .is-protected::before,
            .nocopy::after,
            .nocopy::before {
                pointer-events: none !important;
            }
        `);

        if (Config.HIDE_SPONSORED_MESSAGES) {
            rules.push(`
                .sponsored-post,
                .sponsored-message,
                .channel-ad,
                [data-sponsored="true"] {
                    display: none !important;
                    pointer-events: none !important;
                }
            `);
        }

        if (Config.HIDE_PINNED_MESSAGES) {
            rules.push(`
                .pinned-messages-panel,
                .chat-pinned-message,
                .service-msg-pinned,
                .pinned-message {
                    display: none !important;
                    pointer-events: none !important;
                }
            `);
        }

        if (Config.UNLOCK_SPOILERS) {
            rules.push(`
                .spoiler,
                .spoiler-element,
                .text-spoiler {
                    filter: none !important;
                    background: transparent !important;
                    color: inherit !important;
                    opacity: 1 !important;
                }

                .media-spoiler,
                .media-spoiler-container,
                .spoiler-overlay {
                    display: none !important;
                    opacity: 0 !important;
                    pointer-events: none !important;
                }
            `);
        }

        rules.push(`
            .tue-native-action-restored {
                opacity: 1 !important;
                visibility: visible !important;
                pointer-events: auto !important;
            }
        `);

        rules.push(`
            #telegram-ultimate-enhancer-toast {
                position: fixed;
                top: 16px;
                right: 18px;
                z-index: 2147483000;

                display: none;
                box-sizing: border-box;
                max-width: min(78vw, 360px);
                min-height: 30px;
                padding: 7px 11px;

                border: 1px solid rgba(255,255,255,.14);
                border-radius: 9px;
                background: #202327;
                box-shadow: 0 4px 12px rgba(0,0,0,.20);

                color: #e7e9ee;
                font-family:
                    -apple-system,
                    BlinkMacSystemFont,
                    "Segoe UI",
                    "Microsoft YaHei",
                    sans-serif;
                font-size: 11px;
                font-weight: 650;
                line-height: 1.35;
                text-align: left;

                pointer-events: none;
                user-select: none;
            }
        `);

        rules.push(`
            #tue-feedback-prompt {
                position: fixed;
                right: 16px;
                bottom: 16px;
                z-index: 2147483001;

                box-sizing: border-box;
                width: min(328px, calc(100vw - 32px));
                padding: 14px;

                border: 1px solid #e4e4e7;
                border-radius: 10px;
                background: #ffffff;
                box-shadow: 0 8px 24px rgba(15, 23, 42, .10);

                color: #18181b;
                font-family:
                    -apple-system,
                    BlinkMacSystemFont,
                    "Segoe UI",
                    "Microsoft YaHei",
                    sans-serif;

                text-align: left;
            }

            #tue-feedback-prompt,
            #tue-feedback-prompt * {
                box-sizing: border-box;
            }

            #tue-feedback-prompt .tue-feedback-head {
                display: flex;
                align-items: flex-start;
                justify-content: space-between;
                gap: 10px;
                margin: 0 0 8px;
            }

            #tue-feedback-prompt .tue-feedback-title {
                min-width: 0;
                padding-top: 3px;

                color: #18181b;
                font-size: 15px;
                font-weight: 600;
                line-height: 1.3;
                letter-spacing: -.01em;
            }

            #tue-feedback-prompt .tue-feedback-close {
                flex: 0 0 auto;
                width: 28px;
                height: 28px;
                padding: 0;

                border: 1px solid transparent;
                border-radius: 6px;
                background: transparent;

                color: #71717a;
                font: inherit;
                font-size: 18px;
                font-weight: 400;
                line-height: 1;

                cursor: pointer;
            }

            #tue-feedback-prompt .tue-feedback-close:hover,
            #tue-feedback-prompt .tue-feedback-close:focus-visible {
                border-color: #e4e4e7;
                background: #f4f4f5;
                color: #18181b;
                outline: none;
            }

            #tue-feedback-prompt .tue-feedback-copy {
                color: #52525b;
                font-size: 12.5px;
                font-weight: 400;
                line-height: 1.55;
            }

            #tue-feedback-prompt .tue-feedback-copy p {
                margin: 0 0 8px;
            }

            #tue-feedback-prompt .tue-feedback-copy p:last-child {
                margin-bottom: 0;
            }

            #tue-feedback-prompt .tue-feedback-copy strong {
                color: #18181b;
                font-weight: 600;
            }

            #tue-feedback-prompt .tue-feedback-actions {
                display: grid;
                grid-template-columns: minmax(0, 1fr) 118px;
                gap: 8px;
                margin-top: 12px;
            }

            #tue-feedback-prompt .tue-feedback-action {
                min-width: 0;
                min-height: 36px;
                padding: 7px 10px;

                border-radius: 8px;

                font: inherit;
                font-size: 12.5px;
                font-weight: 500;
                line-height: 1.2;

                cursor: pointer;
            }

            #tue-feedback-prompt .tue-feedback-primary {
                border: 1px solid #18181b;
                background: #18181b;
                color: #fafafa;
            }

            #tue-feedback-prompt .tue-feedback-primary:hover,
            #tue-feedback-prompt .tue-feedback-primary:focus-visible {
                border-color: #27272a;
                background: #27272a;
                outline: none;
            }

            #tue-feedback-prompt .tue-feedback-secondary {
                border: 1px solid #d4d4d8;
                background: #ffffff;
                color: #18181b;
            }

            #tue-feedback-prompt .tue-feedback-secondary:hover,
            #tue-feedback-prompt .tue-feedback-secondary:focus-visible {
                border-color: #a1a1aa;
                background: #f4f4f5;
                outline: none;
            }

            @media (prefers-color-scheme: dark) {
                #tue-feedback-prompt {
                    border-color: #27272a;
                    background: #18181b;
                    box-shadow: 0 8px 24px rgba(0, 0, 0, .28);
                    color: #fafafa;
                }

                #tue-feedback-prompt .tue-feedback-title,
                #tue-feedback-prompt .tue-feedback-copy strong {
                    color: #fafafa;
                }

                #tue-feedback-prompt .tue-feedback-copy {
                    color: #d4d4d8;
                }

                #tue-feedback-prompt .tue-feedback-close {
                    color: #a1a1aa;
                }

                #tue-feedback-prompt .tue-feedback-close:hover,
                #tue-feedback-prompt .tue-feedback-close:focus-visible {
                    border-color: #3f3f46;
                    background: #27272a;
                    color: #fafafa;
                }

                #tue-feedback-prompt .tue-feedback-primary {
                    border-color: #fafafa;
                    background: #fafafa;
                    color: #18181b;
                }

                #tue-feedback-prompt .tue-feedback-primary:hover,
                #tue-feedback-prompt .tue-feedback-primary:focus-visible {
                    border-color: #e4e4e7;
                    background: #e4e4e7;
                }

                #tue-feedback-prompt .tue-feedback-secondary {
                    border-color: #3f3f46;
                    background: #18181b;
                    color: #fafafa;
                }

                #tue-feedback-prompt .tue-feedback-secondary:hover,
                #tue-feedback-prompt .tue-feedback-secondary:focus-visible {
                    border-color: #52525b;
                    background: #27272a;
                }
            }

            @media (max-width: 420px) {
                #tue-feedback-prompt {
                    right: 10px;
                    bottom: 10px;
                    width: calc(100vw - 20px);
                }

                #tue-feedback-prompt .tue-feedback-actions {
                    grid-template-columns: 1fr;
                }
            }
        `);

        GM_addStyle(rules.join('\n'));
    }

    // =============================================================================
    // 12. 生命周期
    // =============================================================================
    const Lifecycle = {
        started: false,

        start() {
            if (this.started || !document.body) return;
            this.started = true;

            NativeActionEngine.initialScan();
            ObserverEngine.start();
            FeedbackPrompt.init();

            Log.info(
                'BOOT',
                'Evidence Native Actions / DOM增量 / 精准事件委托 / GPU Safe UI'
            );
        },

        boot() {
            injectStaticStyle();
            EventHub.init();

            if (document.body) {
                this.start();
                return;
            }

            document.addEventListener(
                'DOMContentLoaded',
                () => this.start(),
                { once: true }
            );
        }
    };

    Lifecycle.boot();
})();