4399-flash-downloader

一键下载 flash 游戏(swf)

当前为 2023-04-28 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            4399-flash-downloader
// @namespace       http://tampermonkey.net/
// @version         0.0.1
// @description     一键下载 flash 游戏(swf)
// @author          [email protected]
// @match           https://www.4399.com/flash/*
// @match           https://s2.4399.com
// @icon            data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzMiIHZpZXdCb3g9IjAgMCAzMiAzMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iMyIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzE3XzQpIi8+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfMTdfNCkiPgo8cGF0aCBkPSJNMyAzSDUuODIzMjNWNy41NDE3N0gxMS41MzU0VjNIMTQuMzU4NlYxNC45Nzk3SDExLjUzNTRWMTAuNDM4SDNWM1pNNS44ODg4OSAxOS45MTY1VjIxLjYyNzhIMTEuNjAxVjE5LjkxNjVINS44ODg4OVpNMjAuMzk5IDE5LjkxNjVWMjEuNjI3OEgyNi4xMTExVjE5LjkxNjVIMjAuMzk5Wk0xNy41NzU4IDE3LjAyMDNIMjlWMjlIMTcuNTc1OFYyNi4xNjk2SDI2LjExMTFWMjQuNDU4MkgxNy41NzU4VjE3LjAyMDNaTTMgMTcuMDIwM0gxNC40MjQyVjI5SDNWMjYuMTY5NkgxMS41MzU0VjI0LjQ1ODJIM1YxNy4wMjAzWk0xNy41NzU4IDNIMjlWMTQuOTc5N0gxNy41NzU4VjEyLjE0OTRIMjYuMTExMVYxMC40MzhIMTcuNTc1OFY3LjYwNzZIMjYuMTExMVY1Ljg5NjJIMTcuNTc1OFYzWiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9kXzE3XzQiIHg9IjIiIHk9IjMiIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR4PSIxIiBkeT0iMiIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIxIi8+CjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC40IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfMTdfNCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd18xN180IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMTdfNCIgeDE9IjMxLjY4IiB5MT0iMzEuMDQiIHgyPSIzLjkxMDA2ZS0wNyIgeTI9Ii0xNy42IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiM1NkFGMEIiLz4KPHN0b3Agb2Zmc2V0PSIwLjk5MjcwOCIgc3RvcC1jb2xvcj0iIzUyQTUwRCIgc3RvcC1vcGFjaXR5PSIwIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==
// @grant           none
// @run-at          document-idle
// @license         GPL-3.0-only
// ==/UserScript==


(function() {
    /**
     * 脚本级全局常量
     */

    BASE_URL = "https://s2.4399.com/4399swf";
    FLASH_ICON = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzE0XzIpIj4KPHBhdGggZD0iTTI4LjI2NjcgMEgzLjczMzMzQzEuNzA2NjcgMCAwIDEuNzA2NjcgMCAzLjczMzMzVjI4LjI2NjdDMCAzMC4yOTMzIDEuNzA2NjcgMzIgMy43MzMzMyAzMkgyOC4yNjY3QzMwLjI5MzMgMzIgMzIgMzAuMjkzMyAzMiAyOC4yNjY3VjMuNzMzMzNDMzIgMS43MDY2NyAzMC4yOTMzIDAgMjguMjY2NyAwWk0yMy40NjY3IDEwLjEzMzNDMjMuNDY2NyAxMC40NTMzIDIzLjI1MzMgMTAuNjY2NyAyMi45MzMzIDEwLjY2NjdDMjAuMjY2NyAxMC42NjY3IDIwLjE2IDEwLjk4NjcgMTkuNzMzMyAxMi4xNkMxOS42MjY3IDEyLjM3MzMgMTkuNjI2NyAxMi41ODY3IDE5LjUyIDEyLjhIMjEuODY2N0MyMi4xODY3IDEyLjggMjIuNCAxMy4wMTMzIDIyLjQgMTMuMzMzM1YxNy42QzIyLjQgMTcuOTIgMjIuMTg2NyAxOC4xMzMzIDIxLjg2NjcgMTguMTMzM0gxOC4wMjY3QzE2Ljg1MzMgMjIuMjkzMyAxMi40OCAyNi42NjY3IDggMjYuNjY2N0M3LjY4IDI2LjY2NjcgNy40NjY2NyAyNi40NTMzIDcuNDY2NjcgMjYuMTMzM1YyMS44NjY3QzcuNDY2NjcgMjEuNTQ2NyA3LjY4IDIxLjMzMzMgOCAyMS4zMzMzQzExLjMwNjcgMjEuMzMzMyAxMi4yNjY3IDE4LjY2NjcgMTMuMzMzMyAxNS4yNTMzQzEzLjU0NjcgMTQuNzIgMTMuNjUzMyAxNC4yOTMzIDEzLjg2NjcgMTMuNzZDMTUuMjUzMyA5LjkyIDE2Ljg1MzMgNS4zMzMzMyAyMi45MzMzIDUuMzMzMzNDMjMuMjUzMyA1LjMzMzMzIDIzLjQ2NjcgNS41NDY2NyAyMy40NjY3IDUuODY2NjdWMTAuMTMzM1oiIGZpbGw9IiNEODFFMDYiLz4KPHBhdGggZD0iTTIxLjE3ODkgMzYuMDg0MkMxOS45MTU4IDM2LjA4NDIgMTguNjUyNiAzNS41Nzg5IDE3LjY0MjEgMzQuNTY4NEMxNS42MjEgMzIuNTQ3NCAxNS42MjEgMjkuMzg5NSAxNy42NDIxIDI3LjM2ODRMMjAuMjk0NyAyNC43MTU4TDIyLjA2MzIgMjYuNDg0MkwxOS40MTA1IDI5LjEzNjhDMTguNCAzMC4xNDc0IDE4LjQgMzEuNjYzMiAxOS40MTA1IDMyLjY3MzdDMjAuNDIxIDMzLjY4NDIgMjEuOTM2OCAzMy42ODQyIDIyLjk0NzQgMzIuNjczN0wyNi40ODQyIDI5LjEzNjhDMjYuOTg5NSAyOC42MzE2IDI3LjI0MjEgMjggMjcuMjQyMSAyNy4zNjg0QzI3LjI0MjEgMjYuNzM2OCAyNi45ODk1IDI2LjEwNTMgMjYuNjEwNSAyNS42TDI1LjIyMTEgMjQuMzM2OEwyNi45ODk1IDIyLjU2ODRMMjguMzc4OSAyMy45NTc5QzI5LjI2MzIgMjQuODQyMSAyOS43Njg0IDI2LjEwNTMgMjkuNzY4NCAyNy40OTQ3QzI5Ljc2ODQgMjguODg0MiAyOS4yNjMyIDMwLjE0NzQgMjguMjUyNiAzMS4wMzE2TDI0LjcxNTggMzQuNTY4NEMyMy44MzE2IDM1LjU3ODkgMjIuNDQyMSAzNi4wODQyIDIxLjE3ODkgMzYuMDg0MlpNMjUuMjIxMSAyOS42NDIxTDIzLjgzMTYgMjguMzc4OUMyMS44MTA1IDI2LjM1NzkgMjEuODEwNSAyMy4yIDIzLjgzMTYgMjEuMTc4OUwyNy4zNjg0IDE3LjY0MjFDMjkuMzg5NSAxNS42MjEgMzIuNTQ3NCAxNS42MjEgMzQuNTY4NCAxNy42NDIxQzM2LjU4OTUgMTkuNjYzMiAzNi41ODk1IDIyLjgyMSAzNC41Njg0IDI0Ljg0MjFMMzEuOTE1OCAyNy40OTQ3TDMwLjE0NzQgMjUuNzI2M0wzMi44IDIzLjA3MzdDMzMuODEwNSAyMi4wNjMyIDMzLjgxMDUgMjAuNTQ3NCAzMi44IDE5LjUzNjhDMzEuNzg5NSAxOC41MjYzIDMwLjE0NzQgMTguNTI2MyAyOS4yNjMyIDE5LjUzNjhMMjUuNiAyMi45NDc0QzI0LjU4OTUgMjMuOTU3OSAyNC41ODk1IDI1LjQ3MzcgMjUuNiAyNi40ODQyTDI2Ljk4OTUgMjcuODczN0wyNS4yMjExIDI5LjY0MjFaIiBmaWxsPSIjMjcyNjM2Ii8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTRfMiI+CjxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K`;


    /**
     * 脚本级公用函数和对象
     */

    /**
     * 元素选择器
     * @param {string} selector 选择器
     * @returns {Array<HTMLElement>} 元素列表
     */
    function $(selector) {
        const self = this?.querySelectorAll ? this : document;
        return [...self.querySelectorAll(selector)];
    }


    /**
     * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒
     * @param {string} selector 选择器
     * @returns {Promise<Array<HTMLElement>>} 元素列表
     */
    async function $$(selector) {
        const self = this?.querySelectorAll ? this : document;

        for (let i = 0; i < 10; i++) {
            let elems = [...self.querySelectorAll(selector)];
            if (elems.length > 0) {
                return elems;
            }
            await new Promise(r => setTimeout(r, 500));
        }
        throw Error(`"${selector}" not found`);
    }


    const util = {
        Socket: class Socket {
            /**
            * 创建套接字对象
            * @param {Window} target 目标窗口
            */
            constructor(target) {
                if (!(target.window && (target === target.window))) {
                    console.log(target);
                    throw new Error(`target is not a [Window Object]`); 
                }
                this.target = target;
                this.connected = false;
                this.listeners = new Set();
            }
        
            get [Symbol.toStringTag]() { return "Socket"; }
        
            /**
            * 向目标窗口发消息
            * @param {*} message 
            */
            talk(message) {
                if (!this.target) {
                    throw new TypeError(
                        `socket.target is not a window: ${this.target}`
                    );
                }
                this.target.postMessage(message, "*");
            }
        
            /**
            * 添加捕获型监听器,返回实际添加的监听器
            * @param {Function} listener (e: MessageEvent) => {...}
            * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器
            * @returns {Function} listener
            */
            listen(listener, once=false) {
                if (this.listeners.has(listener)) {
                    return;
                }
        
                let real_listener = listener;
                // 包装监听器
                if (once) {
                    const self = this;
                    function wrapped(e) {
                        listener(e);
                        self.not_listen(wrapped);
                    }
                    real_listener = wrapped;
                }
                
                // 添加监听器
                this.listeners.add(real_listener);
                window.addEventListener(
                    "message", real_listener, true
                );
                return real_listener;
            }
        
            /**
            * 移除socket上的捕获型监听器
            * @param {Function} listener (e: MessageEvent) => {...}
            */
            not_listen(listener) {
                console.log(listener);
                console.log(
                    "listener delete operation:",
                    this.listeners.delete(listener)
                );
                window.removeEventListener("message", listener, true);
            }
        
            /**
            * 检查对方来信是否为pong消息
            * @param {MessageEvent} e 
            * @param {Function} resolve 
            */
            _on_pong(e, resolve) {
                // 收到pong消息
                if (e.data.pong) {
                    this.connected = true;
                    this.listeners.forEach(
                        listener => listener.ping ? this.not_listen(listener) : 0
                    );
                    console.log("Client: Connected!\n" + new Date());
                    resolve(this);
                }
            }
        
            /**
            * 向对方发送ping消息
            * @returns {Promise<Socket>}
            */
            _ping() {
                return new Promise((resolve, reject) => {
                    // 绑定pong检查监听器
                    const listener = this.listen(
                        e => this._on_pong(e, resolve)
                    );
                    listener.ping = true;
        
                    // 5分钟后超时
                    setTimeout(
                        () => reject(new Error(`Timeout Error during receiving pong (>5min)`)),
                        5 * 60 * 1000
                    );
                    // 发送ping消息
                    this.talk({ ping: true });
                });
            }
        
            /**
            * 检查对方来信是否为ping消息
            * @param {MessageEvent} e 
            * @param {Function} resolve 
            */
            _on_ping(e, resolve) {
                // 收到ping消息
                if (e.data.ping) {
                    this.target = e.source;
                    this.connected = true;
                    this.listeners.forEach(
                        listener => listener.pong ? this.not_listen(listener) : 0
                    );
                    console.log("Server: Connected!\n" + new Date());
                    
                    // resolve 后期约状态无法回退
                    // 但后续代码仍可执行
                    resolve(this);
                    // 回应pong消息
                    this.talk({ pong: true });
                }
            }
        
            /**
            * 当对方来信是为ping消息时回应pong消息
            * @returns {Promise<Socket>}
            */
            _pong() {
                return new Promise(resolve => {
                    // 绑定ping检查监听器
                    const listener = this.listen(
                        e => this._on_ping(e, resolve)
                    );
                    listener.pong = true;
                });
            }
        
            /**
            * 连接至目标窗口
            * @param {boolean} talk_first 是否先发送ping消息
            * @param {Window} target 目标窗口
            * @returns {Promise<Socket>}
            */
            connect(talk_first) {
                // 先发起握手
                if (talk_first) {
                    return this._ping();
                }
                // 后发起握手
                return this._pong();
            }
        },

        /**
         * 以指定原因弹窗提示并抛出错误
         * @param {string} reason 
         */
        raise: function(reason) {
            alert(reason);
            throw new Error(reason);
        },
    
        /**
         * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value]
         * @param {Iterable} iterable 
         * @returns 
         */
        enumerate: function* (iterable) {
            let i = 0;
            for (let value of iterable) {
                yield [i++, value];
            }
        },
    
        /**
         * 同步的迭代若干可迭代对象
         * @param  {...Iterable} iterables 
         * @returns 
         */
        zip: function* (...iterables) {
            // 强制转为迭代器
            const iterators = iterables.map(
                iterable => iterable[Symbol.iterator]()
            );
    
            // 逐次迭代
            while (true) {
                let [done, values] = base.getAllValus(iterators);
                if (done) {
                    return;
                }
                if (values.length === 1) {
                    yield values[0];
                } else {
                    yield values;
                }
            }
        },
    
        /**
         * 返回指定范围整数生成器
         * @param {number} end 如果只提供 end, 则返回 [0, end)
         * @param {number} end2 如果同时提供 end2, 则返回 [end, end2)
         * @param {number} step 步长, 可以为负数,不能为 0
         * @returns 
         */
        range: function*(end, end2=null, step=1) {
            // 参数合法性校验
            if (step === 0) {
                throw new RangeError("step can't be zero");
            }
            const len = end2 - end;
            if (end2 && len && step && (len * step < 0)) {
                throw new RangeError(`[${end}, ${end2}) with step ${step} is invalid`);
            }
    
            // 生成范围
            end2 = end2 === null ? 0 : end2;
            let [small, big] = [end, end2].sort((a, b) => a - b);
            // 开始迭代
            if (step > 0) {
                for (let i = small; i < big; i += step) {
                    yield i;
                }
            } else {
                for (let i = big; i > small; i += step) {
                    yield i;
                }
            };
        },
    
        /**
         * 复制text到剪贴板
         * @param {string} text 
         * @returns 
         */
        copy_text: function(text) {
            // 输出到控制台和剪贴板
            console.log(
                text.length > 20 ?
                    text.slice(0, 21) + "..." : text
            );
            
            if (!navigator.clipboard) {
                base.oldCopy(text);
                return;
            };
    
            navigator.clipboard
                .writeText(text)
                .catch(_ => base.oldCopy(text));
        },
    
        /**
         * 复制媒体到剪贴板
         * @param {Blob} blob
         */
        copy: async function(blob) {
            const data = [new ClipboardItem({ [blob.type]: blob })];
            try {
                await navigator.clipboard.write(data);
                console.log(`${blob.type} 成功复制到剪贴板`);
            } catch (err) {
                console.error(err.name, err.message);
            }
        },
    
        /**
         * 创建并下载文件
         * @param {string} file_name 文件名
         * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容
         * @param {string} type 媒体类型,需要符合 MIME 标准 
         */
        save: function(file_name, content, type="") {
            const blob = new Blob(
                [content], { type }
            );
            const size = (blob.size / 1024).toFixed(1);
            console.log(`blob saved, size: ${size} kb, type: ${blob.type}`);
    
            const url = URL.createObjectURL(blob);
            const a = document.createElement("a");
            a.download = file_name || "未命名文件";
            a.href = url;
            a.click();
            URL.revokeObjectURL(url);
        },
    
        sleep: async function(delay_ms) {
            return new Promise(
                resolve => setTimeout(resolve, delay_ms)
            );
        },
    
        /**
         * 取得get参数key对应的value
         * @param {string} key
         * @returns {string} value
         */
        get_param: function(key) {
            return new URL(location.href).searchParams.get(key);
        },
    
        /**
         * 等待直到函数返回true
         * @param {Function} is_ok 判断条件达成与否的函数
         * @param {number} timeout 最大等待秒数, 默认5000毫秒
         */
        wait_until: async function(is_ok, timeout=5000) {
            const gap = 200;
            let chances = parseInt(timeout / gap);
            chances = chances < 1 ? 1 : chances;
            
            while (! await is_ok()) {
                await this.sleep(200);
                chances -= 1;
                if (!chances) {
                    break;
                }
            }
        },
    
        /**
         * 用try移除元素
         * @param {HTMLElement} element 要移除的元素
         */
        remove: function(element) {
            try {
                element.remove();
            } catch (e) {}
        },
    
        /**
         * 等待全部任务落定后返回值的列表
         * @param {Iterable<Promise>} tasks 
         * @returns {Promise<Array>} values
         */
        gather: async function(tasks) {
            const results = await Promise.allSettled(tasks);
            return results
                .filter(result => result.value)
                .map(result => result.value);
        },
    
        /**
         * 使用xhr异步GET请求目标url,返回响应体blob
         * @param {string} url 
         * @returns {Promise<Blob>} blob
         */
        xhr_get_blob: async function(url) {
            const xhr = new XMLHttpRequest();
            xhr.open("GET", url);
            xhr.responseType = "blob";
            
            return new Promise((resolve, reject) => {
                xhr.onload = () => {
                    const code = xhr.status;
                    if (code >= 200 && code <= 299) {
                        resolve(xhr.response);
                    }
                    else {
                        reject(new Error(`Network Error: ${code}`));
                    }
                }
                xhr.send();
            });
        },
    
        /**
         * 加载CDN脚本
         * @param {string} url 
         */
        load_web_script: async function(url) {
            try {
                // xhr+eval方式
                Function(
                    await (await this.xhr_get_blob(url)).text()
                )();
            } catch(e) {
                console.error(e);
                // 嵌入<script>方式
                const script = document.createElement("script");
                script.src = url;
                document.body.append(script);
            }
        },
    };
    

    /**
     * 域名级主函数
     */


    /**
     * 启动下载 flash 游戏文件
     */
    function dl_flash() {
        /**
         * 域名级全局变量
         */

        let sock;


        async function send_url() {
            const title = $(".name a")[0].textContent.trim() || "flash游戏";
            const path = window._strGamePath;

            if (!path) util.raise(
                "_strGamePath 不存在,找不到游戏文件路径"
            );
            if (!path.endsWith(".swf")) util.raise(
                `当前游戏不是 flash 游戏。\n游戏路径为:${path}`
            );

            const id = "flash-dl-src";
            let iframe = $(`#${id}`)[0];

            if (!iframe) {
                iframe = document.createElement("iframe");
                iframe.id = id;
                iframe.src = "https://s2.4399.com";
                document.body.append(iframe);
                sock = new util.Socket(iframe.contentWindow);
                await sock.connect(false);
            }
            
            sock.talk({
                flash_dl: true,
                url: BASE_URL + path,
                title,
            });
        }

        function add_style() {
            const style = `
            <style>
                #flash-dl-btn {
                    text-align: center;
                    background: url("${FLASH_ICON}");
                    background-repeat: no-repeat;
                    background-position: top;
                    width: 40px;
                    padding-top: 30px;
                    margin: 0 10px;
                    float: left;
                    display: inline;
                    cursor: pointer;
                }

                #flash-dl-src {
                    display: none;
                }
            <style>
            `;
            document.head.insertAdjacentHTML(
                "beforeend", style
            );
        }

        async function add_dl_btn() {
            const box = (await $$("#uplayer .fr"))[0];

            // 修改误导性的下载按钮文本(下载4399游戏盒子)
            $("#down_a")[0].textContent = "盒子";
            
            // 新按钮
            const btn = document.createElement("a");
            btn.id = "flash-dl-btn";
            btn.textContent = "下载";
            btn.onfocus = () => btn.blur();
            btn.onclick = send_url;
            box.insertAdjacentElement("afterbegin", btn);
        }

        (() => {
            console.log("enter: dl_flash");
            add_style();
            add_dl_btn();
        })();
    }

    /**
     * 执行下载 flash 游戏文件
     */
    function dl_flash_in_origin() {
        /**
         * @param {MessageEvent} e 
         */
        async function on_msg(e) {
            if (!e.data.flash_dl) return;

            const { url, title } = e.data;
            const resp = await fetch(url, {
                headers: {
                    "Host": "szhong.4399.com",
                    "X-Requested-With": "ShockwaveFlash/34.0.0.282",
                }
            });
            if (!resp.ok) util.raise(
                `游戏下载失败,错误代码:${resp.status},原因:${resp.statusText}`
            );

            const blob = await resp.blob();
            util.save(title, blob, "application/x-shockwave-flash");
        }

        (() => {
            console.log("enter: dl_flash_in_origin")
            if (window.top === window) return;

            const sock = new util.Socket(window.top);
            sock.listen(on_msg);
            sock.connect(true);
        })();
    }


    /**
     * 路由函数,脚本主函数入口
     */
    function route() {
        console.log("enter: route");

        const host = location.hostname;
        switch (host) {
            case "www.4399.com":
                dl_flash();
                break;

            case "s2.4399.com":
                dl_flash_in_origin();
                break;
        
            default:
                console.log(`不受支持的域名:${host}`);
                break;
        }
    }


    setTimeout(route, 500);

    /**
     * 更新日志
     * ---
     * 更新日期:2023/4/28
     * - 完成第一版  4399 flash 文件下载脚本
     */
})();