shire helper

Download shire thread content.

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 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.

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

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         shire helper
// @namespace    https://greasyfork.org/zh-CN/scripts/461311-shire-helper
// @version      1.0.12.3
// @description  Download shire thread content.
// @author       80824
// @match        https://www.shireyishunjian.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=shireyishunjian.com
// @grant        unsafeWindow
// @grant        GM.getValue
// @grant        GM_getValue
// @grant        GM.setValue
// @grant        GM_setValue
// @grant        GM.deleteValue
// @grant        GM_deleteValue
// @grant        GM.listValues
// @grant        GM_listValues
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM.download
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/loglevel/1.9.2/loglevel.min.js

// ==/UserScript==

(function () {
    'use strict';

    // ========================================================================================================
    // 高频工具
    // ========================================================================================================
    const qS = (selector, scope = document) => scope.querySelector(selector);
    const qSA = (selector, scope = document) => scope.querySelectorAll(selector);
    const docre = tag => document.createElement(tag);
    String.prototype.parseURL = function () {
        const url = new URL(this);
        let obj = { hostname: url.hostname, hash: url.hash, pathname: url.pathname };
        obj.loc = obj.pathname?.match(/([^\/]+)\.[^\.]*/)?.at(1);
        for (let [k, v] of url.searchParams.entries()) {
            obj[k] = v;
        }
        return obj;
    };

    // ========================================================================================================
    // 初始化设置
    // ========================================================================================================
    const helper_default_setting = {
        // 消息通知设置
        enable_notification: true,
        max_noti_threads: 3,
        important_fids: ['78'],
        // 历史记录设置
        enable_history: true,
        max_history: 100,
        // 下载设置
        enable_text_download: true,
        enable_postfile_download: true,
        enable_attach_download: true,
        enable_op_download: true,
        files_pack_mode: 'all',
        default_merge_mode: 'main',
        // 自动回复设置
        enable_auto_reply: false,
        auto_reply_message: '感谢分享!',
        // 自动换行设置
        enable_auto_wrap: false,
        min_wrap_length: 100,
        typical_wrap_length: 200,
        max_wrap_length: 300,
        wrap_dot: '.。??!!',
        wrap_comma: ',,、;;',
        // 代表作设置
        data_cache_time: 168 * 3600 * 1000, // 7天
        masterpiece_num: 10,
        default_masterpiece_sort: 'view',
        // 屏蔽词设置
        enable_block_keyword: false,
        block_keywords: [],
        // 黑名单设置
        enable_blacklist: false,
        blacklist: [],
    };

    const hs = GM_getValue('helper_setting', {});
    let default_updated = false;
    for (let key in helper_default_setting) {
        if (!(key in hs)) {
            hs[key] = helper_default_setting[key];
            log.info(`Setting ${key} not found, set to default value.`);
            default_updated = true;
        }
    }
    if (default_updated) {
        GM.setValue('helper_setting', hs);
    }

    // ========================================================================================================
    // 判断脚本启用条件
    // ========================================================================================================
    const location_params = document.URL.parseURL();
    log.log('Location params:', location_params);

    const is_desktop = location_params.mobile == 'no' || Array.from(qSA('meta')).some(meta => meta.getAttribute('http-equiv') === 'X-UA-Compatible');
    if (location_params.loc != undefined && !is_desktop) {
        log.info('Mobile version detected, shire-helper disabled.');
        return;
    }

    // ========================================================================================================
    // 常量
    // ========================================================================================================
    const mobileUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 Edg/125.0.0.0';
    const large_page_num = 1024;
    const magic_num = Math.sqrt(large_page_num);
    const MIME_type_map = {
        'image/jpeg': 'jpg',
        'image/bmp': 'bmp',
        'image/png': 'png',
        'image/gif': 'gif',
        'image/webp': 'webp',
        'video/mp4': 'mp4',
        'video/x-msvideo': 'avi',
        'video/x-matroska': 'mkv',
        'video/x-flv': 'flv',
        'video/mpeg': 'mpg',
        'video/quicktime': 'mov',
        'audio/mp3': 'mp3',
        'audio/mpeg': 'mp3',
        'audio/wav': 'wav',
        'audio/wave': 'wav',
        'audio/x-wav': 'wav',
        'text/plain': 'txt'
    };

    // ========================================================================================================
    // 通用工具
    // ========================================================================================================
    const decimalCeil = (num, precision = 4) => Math.ceil(num * Math.pow(10, precision)) / Math.pow(10, precision);
    const commonPrefix = ((str1, str2) => {
        [str1, str2] = [str1, str2].map(str => str.replaceAll(/[\s\n\r]/g, '').replaceAll(/[(《\[\{\(\<]/g, '【').replaceAll(/[)》\]\}\)\>]/g, '】'));
        return str1 === '' ? str2 : (str2 === '' ? str1 : (() => {
            let i = 0;
            while (i < str1.length && i < str2.length && str1[i] === str2[i]) i++;
            return str1.slice(0, i);
        })());
    });
    const startWithChinese = str => /^[\p{Script=Han}]/u.test(str);
    const extractFileAndExt = str => {
        const exts = Array.from(Object.values(MIME_type_map)).join('|');
        const regex = new RegExp(`([^\\/]+)\\.(${exts})$`, 'i');
        const match = str.match(regex);
        return match ? [match[1], match[2]] : [str, ''];
    };
    const timeAgo = timestamp => {
        const diff = Date.now() - timestamp;
        const units = [
            { info: '年前', dt: 365 * 24 * 60 * 60 * 1000 },
            { info: '个月前', dt: 30 * 24 * 60 * 60 * 1000 },
            { info: '天前', dt: 24 * 60 * 60 * 1000 },
            // { info: '小时前', dt: 60 * 60 * 1000 },
            // { info: '分钟前', dt: 60 * 1000, bound: 5 * 60 * 1000 }
        ];
        const unit = units.find(({ dt, bound }) => diff >= bound ?? dt); // 如果有bound则以bound为界限,否则以dt为界限
        return unit ? `${Math.floor(diff / unit.dt)}${unit.info}` : '今天内';
    };

    const checkVariableDefined = (variable_name, timeout = 15000, time_interval = 100) => new Promise((resolve, reject) => {
        const startTime = Date.now();

        function check() {
            if (typeof unsafeWindow[variable_name] !== 'undefined') {
                resolve(unsafeWindow[variable_name]);
            } else if (Date.now() - startTime >= timeout) {
                reject(new Error(`Check ${variable_name} timeout exceeded`));
            } else {
                setTimeout(check, time_interval);
            }
        }

        check();
    });

    const executeIfLoctionMatch = (func, params) => {
        const matchFunc = ([k, v]) => {
            const loc_v = location_params[k] == '' ? undefined : location_params[k];
            return Array.isArray(v) ? v.includes(loc_v) : v == loc_v
        };
        if (Object.entries(params).every(matchFunc)) {
            func();
        }
    };

    // ========================================================================================================
    // GM Value 工具
    // ========================================================================================================
    function updateListElements(list, elem, status, equal = (a, b) => a == b) {
        // 根据equal判断独立elem,根据status判断新list中是否有elem
        if (status && !list.some(e => equal(e, elem))) { // 存入元素
            list.push(elem);
        }
        if (list.some(e => equal(e, elem))) {
            const new_list = list.filter(e => !equal(e, elem)); // 删除元素
            list.length = 0;
            list.push(...new_list);
            if (status) {
                list.push(elem); // 更新元素
            }
        }
        return list;
    }

    function updateGMList(list_name, list) {
        if (list.length == 0) {
            GM.deleteValue(list_name);
        }
        else {
            GM.setValue(list_name, list);
        }
    }

    // ========================================================================================================
    // 自定义表情
    // ========================================================================================================
    // const original_smilies_types = ['4'];
    // const new_smilies = [];
    // Element:{name:name, type:type, path:path, info:[[id, smile_code, file_name, width, height, weight]]}
    // Test images: 'data/attachment/album/202207/04/192158kg0urgxtw2805yrs.png','static/image/smiley/ali/1love1.gif',''https://p.upyun.com/demo/webp/webp/animated-gif-0.webp'

    // ========================================================================================================
    // 自定义样式
    // ========================================================================================================

    // 设置弹窗框架
    GM_addStyle(`
            #helper-popup {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 50%;
                min-width: 600px;
                min-height: 300px;
                max-height: 85%;
                background-color: white;
                color: black !important;
                border: 1px solid #ccc;
                z-index: 2000;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
                border-radius: 12px;
                display: flex;
                flex-direction: column;
                overflow: hidden;
            }

            #helper-title-container {
                display: flex;
                align-items: center;
                padding: 20px;
                font-size: 1.5rem;
                font-weight: bold;
                text-align: left;
                border-bottom: 1px solid #ccc;
            }

            #helper-title {
                flex: 1;
            }

            #helper-content-container {
                display: flex;
                flex: 1;
                overflow: hidden;
                background-color: inherit;
            }

            #helper-tab-btn-container {
                display: flex;
                flex-direction: column;
                flex-shrink: 0;
            }

            #helper-tab-content-container {
                flex: 1;
                padding: 10px;
                font-size: 0.75rem;
            }`);

    // 消息弹窗
    GM_addStyle(`
            #helper-notification-popup {
                position: fixed;
                bottom: 20px;
                right: 20px;
                width: 35%;
                min-width: 300px;
                max-height: 80%;
                background-color: rgba(0, 0, 0, 0.9);
                color: white;
                padding: 20px;
                border-radius: 5px;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
                z-index: 10000;
            }

            .helper-noti-message {
                width: 100%;
                overflow: hidden;
                white-space: nowrap;
            }`);

    // 弹窗关闭按钮
    GM_addStyle(`
            .helper-close-btn {
                border: none;
                cursor: pointer;
                margin-left: 10px;
                border-radius: 50%;
                width: 30px;
                height: 30px;
                line-height: 30px;
                text-align: center;
                transition: background-color 0.3s;
            }

            .helper-close-btn {
                background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>');
            }

            .helper-close-btn.helper-redx {
                background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>');
            }

            .helper-close-btn:hover {
                background-color: #ddd;
            }`);

    // 蒙版与加载动画
    GM_addStyle(`
            #helper-loading-overlay{
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background-color: rgba(255, 255, 255, 0.7);
                display: flex;
                justify-content: center;
                align-items: center;
                z-index: 3000;
            }

            #helper-overlay {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
            }

            #helper-overlay.helper-redirect-layer{
                cursor: pointer;
                background-color: transparent;
            }

            #helper-overlay.helper-shield-layer{
                background-color: rgba(0, 0, 0, 0.5);
                z-index: 1000;
            }

            .helper-spinner {
                border: 5px solid #f3f3f3;
                border-top: 5px solid #3498db;
                border-radius: 50%;
                width: 50px;
                height: 50px;
                animation: spin 1s linear infinite;
            }

            @keyframes spin {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }`);

    // 顶部进度条
    GM_addStyle(`
            #helper-top-progressbar-container {
                position: fixed;
                top: 0;
                width: 100%;
                background-color: #eee;
                z-index: 3000;
            }

            #helper-top-progressbar {
                height: 5px;
                background-color: #4caf50;
                width: 0%;
                transition: width 0.5s ease-in-out
            }`);

    // 开关控件
    GM_addStyle(`
            label:has(> .helper-toggle-switch) > input {
                display: none;
            }

            .helper-toggle-switch {
                position: relative;
                display: inline-block;
                width: 32px;
                background-color: #ddd;
                transition: background-color 0.3s;
            }

            .helper-toggle-switch::after {
                content: '';
                position: absolute;
                top: 2px;
                left: 2px;
                width: 12px;
                height: 12px;
                background-color: white;
                border-radius: 50%;
                transition: transform 0.3s;
            }

            label:has(> .helper-toggle-switch) > input:checked + .helper-toggle-switch {
                background-color: #4caf50;
            }

            label:has(> .helper-toggle-switch) > input:checked + .helper-toggle-switch::after {
                transform: translateX(15px);
            }`);

    // 下拉控件
    GM_addStyle(`
            .helper-select {
                appearance: none;
                -webkit-appearance: none;
                -moz-appearance: none;
                background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="black" class="bi bi-chevron-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/></svg>')
                no-repeat right 10px center;
                background-color: inherit;
                color: inherit;
                border: 1px solid #ccc;
                padding: 0 30px 0 10px;
                width: max-content;
                transition: background-color 0.3s, border-color 0.3s;
                cursor: pointer;
                outline: none;
            }

            .helper-select:focus {
                background-color: #ddd;
                border-color: #ccc;
            }`);

    // 多选控件
    GM_addStyle(`
            .helper-multicheck-container {
                display: flex;
                border: 1px solid #ccc;
                box-sizing: border-box;
                overflow: hidden;
            }

            .helper-multicheck-item {
                flex: 1;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                transition: background-color 0.3s;
                position: relative;
            }

            .helper-multicheck-item:not(:first-child)::before {
                content: '';
                position: absolute;
                top: 0;
                bottom: 0;
                left: 0;
                width: 1px;
                background-color: #eee;
            }

            .helper-multicheck-item:not(:first-child) {
                padding-left: 1px;
            }

            .helper-multicheck-item:not(:last-child)::before {
                display: block;
            }

            .helper-multicheck-item input[type="checkbox"] {
                position: absolute;
                opacity: 0;
                width: 100%;
                height: 100%;
                margin: 0;
                cursor: pointer;
            }

            .helper-multicheck-item input[type="checkbox"]:checked + .helper-multicheck-text {
                background-color: #4caf50;
            }

            .helper-multicheck-item .helper-multicheck-text {
                display: flex;
                align-items: center;
                justify-content: center;
                height: 100%;
                padding: 0 10px;
                background-color: inherit;
                border: 1px solid transparent;
                transition: background-color 0.3s;
                box-sizing: border-box;
                white-space: nowrap;
            }`);

    // 单行输入控件
    GM_addStyle(`
            .helper-input {
                width: 80%;
                height: 2em;
                border: 1px solid #ccc;
                border-radius: 5px;
                padding-left: 5px;
                margin-left: 5px;
            }`);

    // 标签控件
    GM_addStyle(`
            .helper-tag-container {
                display: flex;
                flex-wrap: wrap;
                margin-top: 10px;
            }

            .helper-tag {
                background-color: #d2553d;
                color: white;
                padding: 2px 5px 2px 10px;
                margin: 5px;
                border-radius: 20px;
                display: flex;
                align-items: center;
            }

            .helper-tag .helper-tag-remove-btn {
                background-color: transparent;
                border: none;
                color: white;
                margin-left: 5px;
                cursor: pointer;
            }`);

    // 执行按钮控件
    GM_addStyle(`
            .helper-setting-button {
                padding: 0 20px;
                background-color: inherit;
                color: inherit;
                border: 1px solid #ccc;
                cursor: pointer;
                transition: background-color 0.3s;
            }`);

    // 关注、屏蔽按钮控件
    GM_addStyle(`
            .helper-f-button,
            .helper-b-button {
                padding: 2px;
                width: 5.5rem;
                cursor: pointer;
                border: none;
                border-radius: 8px;
                color: white;
                transition: background-color 0.3s ease;
            }

            .helper-b-button {
                background-color: #d2553d;
            }

            .helper-b-button::before {
                content: '已屏蔽';
            }

            .helper-f-button:not([data-hfb-followed]) {
                background-color: #1772f6;
            }

            .helper-f-button:not([data-hfb-followed]):hover {
                background-color: #0063e6;
            }

            .helper-f-button[data-hfb-followed] {
                background-color: #8491a5;
            }

            .helper-f-button[data-hfb-followed]:hover {
                background-color: #758195;
            }

            .helper-f-button.hfb-normal:not([data-hfb-followed])::before {
                content: '关注';
            }

            .helper-f-button.hfb-special:not([data-hfb-followed])::before {
                content: '特别关注';
            }

            .helper-f-button.hfb-thread:not([data-hfb-followed])::before {
                content: '在本帖关注';
            }

            .helper-f-button.hfb-normal[data-hfb-followed]::before {
                content: '已关注';
            }

            .helper-f-button.hfb-normal[data-hfb-followed]:hover::before {
                content: '取消关注';
            }

            .helper-f-button.hfb-special[data-hfb-followed]::before {
                content: '已特关';
            }

            .helper-f-button.hfb-special[data-hfb-followed]:hover::before {
                content: '取消特关';
            }

            .helper-f-button.hfb-thread[data-hfb-followed]::before {
                content: '已在本帖关注';
            }

            .helper-f-button.hfb-thread[data-hfb-followed]:hover::before {
                content: '在本帖取关';
            }`);

    // 复选框控件
    GM_addStyle(`
            .helper-checkbox {
                appearance: none;
                width: 10px;
                height: 10px;
                border: 1px solid black;
                background-color: transparent;
                display: inline-block;
                position: relative;
                margin-right: 5px;
                cursor: pointer;
            }

            .helper-checkbox:before {
                content: '';
                background-color: black;
                display: block;
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%) scale(0);
                width: 5px;
                height: 5px;
                transition: all 0.1s ease-in-out;
            }

            .helper-checkbox:checked:before {
                transform: translate(-50%, -50%) scale(1);
            }

            .helper-checkbox-label {
                color: black;
                cursor: pointer;
                user-select: none;
                display: flex;
                align-items: center;
            }`);

    // 控件通用样式
    GM_addStyle(`
            .helper-active-component {
                height: 32px;
                border-radius: 32px;
            }

            .helper-halfheight-active-component {
                height: 16px;
                border-radius: 16px;
            }`);

    // 弹窗表格
    GM_addStyle(`
            .helper-popup-table {
                width: 100%;
                height: 100%;
                border-collapse: collapse;
            }

            .helper-scroll-component:has(> .helper-popup-table) {
                padding-top: 0 !important;
            }

            .helper-popup-table th,
            .helper-popup-table td {
                padding: 8px;
                text-align: center;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                max-width: 10rem;
            }

            .helper-sticky-header {
                position: sticky;
                top: 0px;
                padding-top: 10px;
                background-color: white;
            }

            th.helper-sortby::after {
                content: '▼';
            }`);

    // 弹窗表格中复用移动版主题span
    GM_addStyle(`
            .helper-popup-table .micon{
                background-color: #6db1d5;
                color: white;
                padding: 1px;
                margin-right: 3px;
                border-radius: 2px;
                overflow: hidden;
            }

            .helper-popup-table .top{
                background-color: #ff9900;
            }

            .helper-popup-table .lock{
                background-color: #ff5656;
            }

            .helper-popup-table .digest{
                background-color: #b3cc0d;
            }`);

    // 其它弹窗样式
    GM_addStyle(`
            .helper-hr {
                margin: 0;
                border: 0;
                border-top: 1px solid #ccc;
            }

            .helper-scroll-component {
                overflow-y: auto;
                scrollbar-width: thin;
                scrollbar-color: #888 #eee;
            }

            .helper-tab-btn {
                padding: 10px;
                border: none;
                background-color: transparent;
                color: inherit;
                cursor: pointer;
                text-align: center;
                font-size: 0.75rem;
                font-weight: 500;
                margin: 5px;
                border-radius: 12px;
                transition: background-color 0.3s;
                white-space: nowrap;
            }

            .helper-tab-selected {
                background-color: #ddd;
            }

            div.helper-center-message {
                display: flex;
                justify-content: center;
                align-items: center;
                height: 100%;
                width: 100%;
                padding: 0;
            }

            .helper-footnote {
                position: absolute;
                bottom: 2px;
                left: 5px;
                font-size: 70%;
                color: #ccc;
                background-color: inherit;
            }

            .helper-setting-container {
                display: flex;
                min-height: 36px;
                justify-content: space-between;
                align-items: center;
                padding: 5px;
            }

            div:has(> .helper-setting-container) .helper-setting-container:not(:last-of-type) {
                border-bottom: 1px solid #ccc;
            }`);



    // 其它样式
    GM_addStyle(`
            .helper-ellip-link {
                display: inline-block;
                color: #004e83 !important;
                overflow: hidden;
                max-width: calc(min(70%, 30rem));
                text-overflow: ellipsis;
                vertical-align: top;
            }`);

    // ========================================================================================================
    // 获取页面信息
    // ========================================================================================================
    function hasReadPermission(doc = document) {
        return !Boolean(qS('#messagetext', doc));
    }

    function isLogged() {
        return document.cookie.split(';').some(e => /_lastcheckfeed=\d+/.test(e));
    }

    function isFirstPage(URL_params) {
        const page = URL_params.page;
        return !Boolean(page) || page == 1;
    }

    function getPostId(post) { return post.id.slice(3); }

    function getPostsInPage(page_doc = document) { return qSA('[id^=pid]', page_doc); }

    function getPostInfo(post) {
        const post_id = getPostId(post);
        const post_URL = qS(`#postnum${post_id}`, post).href;
        const post_URL_params = post_URL.parseURL();
        const thread_id = post_URL_params.ptid;
        const post_auth = qS('#favatar' + post_id + ' > div.pi > div > a', post).text;
        const post_auth_id = qS('#favatar' + post_id + ' > div.pi > div > a', post).href.parseURL().uid;
        const sub_time = qS('[id^=authorposton]', post).textContent;

        return { post_id, thread_id, post_auth, post_auth_id, sub_time, post_URL };
    }

    function getFirstFloorAuthorInfo(page_doc = document) {
        const first_post_info = getPostInfo(qS('#postlist > div > table'), page_doc);
        return { name: first_post_info.post_auth, uid: first_post_info.post_auth_id };
    }

    function theOnlyAuthorInfo(page_doc = document) {
        const specific_authorid = page_doc.original_url.parseURL().authorid;
        const first_floor_author = getFirstFloorAuthorInfo(page_doc);
        return specific_authorid == first_floor_author.uid ? first_floor_author : null;
    }

    async function getThreadAuthorInfo(page_doc = document) {
        const URL_params = page_doc.original_url.parseURL();

        if (isFirstPage(URL_params)) {
            const the_only_author = theOnlyAuthorInfo(page_doc);
            if (!the_only_author) {
                return getFirstFloorAuthorInfo();
            }
            else {
                delete URL_params.authorid;
                const real_first_page = await getPageDocInDomain(URL_params);
                return getThreadAuthorInfo(real_first_page);
            }
        }
        else {
            const thread_auth_name = qS('#tath > a:nth-child(1)').title;
            const thread_auth_id = qS('#tath > a:nth-child(1)').href.parseURL().uid;
            return { name: thread_auth_name, uid: thread_auth_id };
        }
    }

    function getSpaceAuthor(page_doc = document) {
        const URL_params = page_doc.original_url.parseURL();

        if (typeof URL_params.do === 'undefined') {
            return qS('meta[name="keywords"]', page_doc).content.slice(0, -3);
        }
        else {
            const author_name = qS('#pcd > div > div > h2 > a', page_doc);
            return author_name ? author_name.textContent : '';
        }
    };

    async function getThreadPopularity(tid) {
        const page_doc = await getPageDocInDomain({ loc: 'forum', mod: 'viewthread', tid, mobile: '2' }, mobileUA);
        const views = Number(qS('i.dm-eye', qS('ul.authi', page_doc)).nextSibling.textContent);
        const replies = Number(qS('i.dm-chat-s', qS('ul.authi', page_doc)).nextSibling.textContent);
        const favs = Number(qS('#forum > div.foot.foot_reply.flex-box.cl > a:nth-child(2)', page_doc).textContent.slice(0, -2));
        const shares = Number(qS('#forum > div.foot.foot_reply.flex-box.cl > a:nth-child(3)', page_doc).textContent.slice(0, -2));
        return { views, replies, favs, shares };
    }

    // ========================================================================================================
    // 获取页面内容
    // ========================================================================================================
    function createURLInDomain(params) {
        if (!'loc' in params) {
            return;
        }
        let url = `https://${location.host}/main/${params.loc}.php?`;
        delete params.loc;
        for (let [key, value] of Object.entries(params)) {
            url += `${key}=${value}&`;
        }
        return url;
    }

    async function getPageDocInDomain(params, UA = null) {
        const url = createURLInDomain(params);
        if (UA === null) {
            const response = await fetch(url);
            let page_doc = new DOMParser().parseFromString(await response.text(), 'text/html');
            page_doc.original_url = url;
            return page_doc;
        }
        else {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    headers: { 'User-Agent': UA },
                    onload: response => {
                        let page_doc = new DOMParser().parseFromString(response.responseText, 'text/html');
                        page_doc.original_url = url;
                        resolve(page_doc);
                    }
                });
            });
        }
    }

    function formatPostNodeText(node) {
        let text = '';
        switch (node.tagName + '.' + node.className) {
            case 'STYLE.':
            case 'SCRIPT.':
            case 'TABLE.op':
            case 'IGNORE_JS_OP.':
                break;
            case 'DIV.quote':
                text += '<<<\n';
                let quote_href = qS('td > div > blockquote > font > a', node);
                if (quote_href) {
                    let origin_quote = quote_href.innerText;
                    quote_href.innerText += ` PID:${quote_href.href.parseURL().pid}`;
                    text += node.textContent + '\n';
                    quote_href.innerText = origin_quote;
                }
                else {
                    text += node.textContent + '\n'
                }
                text += '>>>\n';
                break;
            case 'HR.l':
                text += '++++++++\n';
                break;
            default:
                text += node.textContent;
        }
        return text;
    }

    function getPostContent(pid, page_doc = document) {
        const post = qS('#post_' + pid, page_doc);

        const tf = qS('#postmessage_' + pid, post);
        let children_nodes = tf.childNodes;
        let text = '';
        for (let child of children_nodes) {
            text += formatPostNodeText(child);
        }

        let post_file = [];
        qSA('ignore_js_op', tf).forEach(node => {
            for (let a of qSA('a', node)) {
                if (a.href.includes('mod=attachment&aid=')) {
                    let title, ext;
                    if (a.innerText == '下载附件') {
                        [title, ext] = extractFileAndExt(qS('strong', node).innerText);
                    }
                    else {
                        [title, ext] = extractFileAndExt(a.innerText);
                    }

                    if (!startWithChinese(title)) {
                        title = '';
                    }

                    post_file.push({ url: a.href, title, ext });
                    break;
                }
            }
        });

        let attach = [];
        qSA('a[id^=aid]', post).forEach(a => {
            let [title, ext] = extractFileAndExt(a.innerText);
            if (!startWithChinese(title)) {
                title = '';
            }
            attach.push({ url: a.href, title, ext });
        });

        // let image_list = qS('#imagelist_' + pid, post) // 多图
        // if (image_list) {
        //     image_list = qS('div.pattl', post); // 单图
        // }
        // if (image_list) {
        //     image_list = qSA('img', image_list);
        //     for (let i = 0; i < image_list.length; i++) {
        //         const img = image_list[i];
        //         const img_url = img.getAttribute('zoomfile');
        //         let img_title = img.title
        //         if (!startWithChinese(img_title)) {
        //             img_title = '';
        //         }
        //         attach.push({ url: img_url, title: img_title });
        //     }
        // }

        let op_body = qS('[id^="op-"][id$="-body"]', post);
        let op = [];
        if (op_body) {
            const url_list = qSA('a', op_body);
            if (url_list.length > 0) {
                for (let url of url_list) {
                    let [title, ext] = extractFileAndExt(url.innerText);
                    op.push({ url: url.href, title, ext });
                }
            }
        }

        return { text, post_file, attach, op };
    }

    async function getPageContent(page_doc, type = 'main') { // type: main, checked, all
        if (!page_doc.original_url) {
            page_doc.original_url = page_doc.URL;
        }

        const tid = page_doc.original_url.parseURL().tid;
        const title = qS('meta[name="keywords"]', page_doc).content;
        let page_id = page_doc.original_url.parseURL().page;
        if (!page_id) {
            page_id = 1;
        }
        const checked_posts = await GM.getValue(tid + '_checked_posts', []);
        const posts_in_page = getPostsInPage(page_doc);

        let text = '';
        let post_file = [];
        let attach = [];
        let op = [];
        for (let post of posts_in_page) {
            if (type == 'checked') {
                const post_id = getPostId(post);
                if (!checked_posts.includes(post_id)) {
                    continue;
                }
            }
            const post_info = getPostInfo(post);
            const post_content = getPostContent(post_info.post_id, page_doc);

            post_file.push(...post_content.post_file);
            attach.push(...post_content.attach);
            op.push(...post_content.op);

            if (type != 'main') {
                text += '<----------------\n';
            }
            text += `//${post_info.post_auth}(UID: ${post_info.post_auth_id}) ${post_info.sub_time}\n`;
            text += `//PID:${post_info.post_id}\n`;
            text += post_content.text;
            if (type != 'main') {
                text += '\n---------------->\n';
            }

            if (type == 'main') {
                break;
            }
        }
        return { tid, title, page_id, text, post_file, attach, op };
    }

    async function getAllPageContent(tid, authorid = '', type = 'all', progress = null, dt = null) { // type: main, all, checked
        const first_page = await getPageDocInDomain({ loc: 'forum', mod: 'viewthread', tid, authorid });
        const title = qS('meta[name="keywords"]', first_page).content;
        if (!hasReadPermission(first_page)) {
            updateProgressbar(progress, dt);
            return { tid, title, text: '没有阅读权限', attach: [], op: [] };
        }

        if (type == 'main') {
            updateProgressbar(progress, decimalCeil(0.8 * dt));
            const content = await getPageContent(first_page, type);
            updateProgressbar(progress, decimalCeil(0.2 * dt));
            return content;
        }

        const page_num = (qS('#pgt > div > div > label > span', first_page) || { title: '共 1 页' }).title.match(/共 (\d+) 页/)[1];
        const ddt = decimalCeil(dt / page_num);
        updateProgressbar(progress, ddt);

        const promises = [getPageContent(first_page, type)];
        promises.push(...Array.from({ length: page_num - 1 }, async (_, i) => {
            const page = await getPageDocInDomain({ loc: 'forum', mod: 'viewthread', tid, page: i + 2, authorid });
            updateProgressbar(progress, 0.8 * ddt);
            const content = await getPageContent(page, type);
            updateProgressbar(progress, 0.2 * ddt);
            return content;
        }));

        let content_list = await Promise.all(promises);
        content_list.sort((a, b) => a.page_id - b.page_id);

        let text = '';
        let post_file = [];
        let attach = [];
        let op = [];
        content_list.forEach(content => {
            text += content.text + '\n';
            post_file.push(...content.post_file);
            attach.push(...content.attach);
            op.push(...content.op);
        });

        return { tid, title, text, post_file, attach, op };
    }

    // ========================================================================================================
    // 保存与下载
    // ========================================================================================================
    function downloadFromURL(target, zip = null, progress = null, dt = null) {
        // 直接下载或者保存到zip
        // progress={value} 当前进度条百分比
        // dt 完成后增加的进度条百分比
        let { url, title, ext, is_blob } = target;

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                onload: response => {
                    if (!ext) {
                        const content_type = response.responseHeaders.match(/Content-Type: (.+)/i);
                        if (content_type && content_type[1]) {
                            ext = MIME_type_map[content_type[1]] || '';
                        }
                    }

                    if (ext != '') {
                        const blob = response.response;
                        if (zip !== null && response.status == 200) {
                            zip.file(`${title}.${ext}`, blob, { binary: true });
                            updateProgressbar(progress, dt);
                        }
                        else {
                            const reader = new FileReader();
                            reader.readAsDataURL(blob);
                            reader.onload = () => {
                                const a = docre('a');
                                a.download = `${title}.${ext}`;
                                a.href = reader.result;
                                a.click();
                                updateProgressbar(progress, dt);
                            }
                            const revokeURL = () => is_blob ? URL.revokeObjectURL(url) : null;
                            reader.onloadend = revokeURL;
                        }
                        resolve();
                    }
                }
            });
        });
    }

    async function insertZip(target_list, zip, progress = null, dt = null) {
        const calcZipTagrgetFileNum = target_list => {
            if (!target_list.hasOwnProperty('files_num')) {
                target_list.files_num = target_list.reduce((acc, cur) => acc + (cur?.is_dir ? calcZipTagrgetFileNum(cur.files) : 1), 0);
            }
            return target_list.files_num;
        };
        calcZipTagrgetFileNum(target_list);

        const ddt = dt ? dt / target_list.files_num : null;
        for (let target of target_list) {
            if (target?.is_dir) {
                const dir = zip.folder(target.title);
                insertZip(target.files, dir, progress, ddt * target.files_num);
            }
            else {
                await downloadFromURL(target, zip, progress, ddt);
            }
        }
    }

    async function createZipAndDownloadFromURLs(zip_name, target_list, progress = null, dt = null) {
        if (target_list.length == 0) {
            return Promise.resolve();
        }

        if (target_list.length == 1) {
            return downloadFromURL(target_list[0], null, progress, dt);
        }

        const zip = new JSZip();
        await insertZip(target_list, zip, progress, decimalCeil(0.75 * dt));
        return await new Promise(async (resolve, reject) => {
            const content = await zip.generateAsync({ type: 'blob' });
            const a = docre('a');
            a.download = zip_name + '.zip';
            a.href = URL.createObjectURL(content);
            a.click();
            updateProgressbar(progress, decimalCeil(0.25 * dt));
            URL.revokeObjectURL(a.href);
            resolve();
        });
    }

    async function saveFile(filename, text, post_file = [], attach = [], op = []) {
        let download_list = []

        if (hs.enable_text_download) {
            const blob = new Blob([text], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            download_list.push({ list: [{ url, title: filename, is_blob: true }], name: '正文' });
        }

        if (hs.enable_postfile_download) {
            post_file.forEach((e, i) => {
                if (!e.url.startsWith(location.origin)) {
                    e.url = location.origin + '/main/' + e.url;
                }
                e.title = e.title || `${i + 1}`;
                e.title = `${filename}_${e.title}`;
            });
            download_list.push({ list: post_file, name: '帖内资源' });
        }

        if (hs.enable_attach_download) {
            attach.forEach((e, i) => {
                if (!e.url.startsWith(location.origin)) {
                    e.url = location.origin + '/main/' + e.url;
                }
                e.title = e.title || `${i + 1}`;
                e.title = `${filename}_附${e.title}`;
            });
            download_list.push({ list: attach, name: '附件' });
        }

        if (hs.enable_op_download) {
            download_list.push({ list: op, name: '原创资源保护' });
        }

        download_list = download_list.filter(e => e.list.length > 0);
        if (download_list.length == 0) {
            alert('没有需要保存的内容, 请检查设置.');
            return;
        }

        const top_progressbar = createTopProgressbar();
        let progress = { value: 0, bar: top_progressbar };
        let promises = [];

        switch (hs.files_pack_mode) {
            case 'no': {
                const files_num = download_list.reduce((acc, cur) => acc + cur.list.length, 0);
                const dt = decimalCeil(100 / files_num);
                download_list.forEach(target =>
                    target.list.forEach(e =>
                        promises.push(downloadFromURL(e, null, progress, dt)
                        )));
                break;
            }
            case 'single': {
                const dt = decimalCeil(100 / download_list.length);
                download_list.forEach(target =>
                    promises.push(createZipAndDownloadFromURLs(`${filename}_${target.name}`, target.list, progress, dt)
                    ));
                break;
            }
            case 'all': {
                promises.push(createZipAndDownloadFromURLs(filename, download_list.flatMap(e => e.list), progress, 100));
                break;
            }
        }
    }

    async function saveThread(type = 'main') {
        const thread_id = qS('#postlist > table:nth-child(1) > tbody > tr > td.plc.ptm.pbn.vwthd > span > a').href.parseURL().tid;
        let title_name = qS('#thread_subject').parentNode.textContent.replaceAll('\n', '').replaceAll('[', '【').replaceAll(']', '】');
        let file_info = `Link: ${document.URL}\n****************\n`;

        if (type == 'main') {
            let text = file_info;
            let content = await getPageContent(document, 'main');
            text += content.text;
            await saveFile(title_name, text, content.post_file, content.attach, content.op);
        }
        else {
            if (type == 'checked') {
                const checked_posts = await GM.getValue(thread_id + '_checked_posts', []);
                if (checked_posts.length == 0) {
                    alert('没有需要保存的内容, 请检查设置.');
                    return;
                }
            }

            let filename = title_name;
            let text = file_info;

            const the_only_author = theOnlyAuthorInfo();

            let filename_suffix = '';
            if (the_only_author) {
                filename_suffix = `${the_only_author.name}`;
                if (type == 'checked') {
                    filename_suffix += '节选';
                }
            }
            else {
                if (type == 'all') {
                    filename_suffix = '全帖';
                }
                else if (type == 'checked') {
                    filename_suffix = '节选';
                }
            }
            filename += '(' + filename_suffix + ')';

            const content_list = await getAllPageContent(thread_id, the_only_author ? the_only_author.uid : '', type);
            await saveFile(filename, text + content_list.text, content_list.post_file, content_list.attach, content_list.op);
            if (type == 'checked') {
                GM.deleteValue(thread_id + '_checked_posts');
            }
        }
    }

    async function saveMergedThreads(type = 'main') {
        const uid = location_params.uid;
        let checked_threads = GM_getValue(uid + '_checked_threads', []);

        if (checked_threads.length == 0) {
            alert('没有需要保存的内容, 请检查设置.');
            return;
        }

        const bar = createTopProgressbar();
        const progress = { value: 0, bar };

        if (type == 'main') {
            const dt = decimalCeil(90 / checked_threads.length);
            const promises = checked_threads.map(tid => getAllPageContent(tid, uid, 'main', progress, dt));
            let content_list = await Promise.all(promises);
            content_list = content_list.sort((a, b) => a.tid - b.tid);
            const content = content_list.map(e => e.text).join('\n\n=================\n\n');
            let filename = content_list.reduce((acc, cur) => commonPrefix(acc, cur.title), content_list[0].title);
            filename += '(合集)';
            await downloadFromURL({
                url: URL.createObjectURL(new Blob([content], { type: 'text/plain' })),
                title: filename,
                is_blob: true
            },
                null, progress, 10);
        }
        else {
            const dt = decimalCeil(85 / checked_threads.length);
            const promises = checked_threads.map(tid => getAllPageContent(tid, type == 'author' ? uid : '', type, progress, dt));
            let content_list = await Promise.all(promises);
            let filename = content_list.reduce((acc, cur) => commonPrefix(acc, cur.title), content_list[0].title);
            filename += type == 'author' ? '(合集仅作者)' : '(合集)'
            content_list = content_list.map(e => {
                return {
                    title: e.title,
                    url: URL.createObjectURL(new Blob([e.text], { type: 'text/plain' }))
                }
            });
            createZipAndDownloadFromURLs(filename, content_list, progress, 15);
        }

        await GM.deleteValue(uid + '_checked_threads');
        updatePageDoc();
    }

    // ========================================================================================================
    // 获取用户最新动态
    // ========================================================================================================
    async function getUserNewestPostOrThread(uid, tid, last_tpid = 0) {
        // 返回结构:
        // { new: [{ tid, title, pids: [], fid }], found, last_tpid }
        // 其中对于对于tid=0的情况,pids为undefined; 对于tid!=0的情况,fid为undefined
        if (tid == 0) {
            return getUserNewestThread(uid, last_tpid);
        }
        else if (tid > 0) {
            return getUserNewestPostInThread(uid, tid, last_tpid);
        }
        else if (tid == -1) {
            return getUserNewestReply(uid, last_tpid);
        }
    }

    async function getUserNewestReply(uid, last_pid = 0) {
        // 返回用户空间回复页首页新于last_pid的回复,通过能否在首页查询到不晚于last_pid的回复判断是否可能有更多回复

        const URL_params = { loc: 'home', mod: 'space', uid, do: 'thread', view: 'me', type: 'reply', from: 'space', mobile: '2' };
        const followed_threads = GM_getValue(uid + '_followed_threads', []);
        const follow_tids = followed_threads.map(e => e.tid).filter(e => e > 0);
        const page_doc = await getPageDocInDomain(URL_params, mobileUA);
        const threads_in_page = qSA('#home > div.threadlist.cl > ul > li', page_doc);
        let new_replyed_threads = [];
        let found = false;
        if (threads_in_page.length > 0) {
            for (let thread of threads_in_page) {
                const reply_in_thread = qSA('a', thread);
                const tid = reply_in_thread[0].href.parseURL().ptid;
                const title = qS('em', reply_in_thread[0]).textContent.trim()
                let pids = []
                for (let i = 1; i < reply_in_thread.length; i++) { // index 0 是主题链接
                    const pid = reply_in_thread[i].href.parseURL().pid;
                    if (pid <= last_pid) {
                        found = true;
                        break;
                    }
                    pids.push(pid);
                }
                if (pids.length > 0 && !follow_tids.includes(Number(tid))) {
                    new_replyed_threads.push({ tid, title, pids });
                }
            }
        }
        last_pid = new_replyed_threads.length == 0 ? 1 : new_replyed_threads[0].pids[0]; // last_pid==0代表第一次查询新回复状态,所以完全没有回复的状态只能设为1
        return { 'new': new_replyed_threads, found, last_tpid: last_pid };
    }

    async function getUserNewestThread(uid, last_tid = 0) {
        // 返回用户空间主题页首页新于last_tid的主题,通过能否在首页查询到不晚于last_tid的主题判断是否可能有更多主题

        const URL_params = { loc: 'home', mod: 'space', uid, do: 'thread', view: 'me', from: 'space', mobile: '2' };
        const page_doc = await getPageDocInDomain(URL_params, mobileUA);
        const threads_in_page = qSA('li.list', page_doc);
        let new_threads = [];
        let found = false;
        if (threads_in_page.length > 0) {
            for (let thread of threads_in_page) {
                const thread_title_node = qS('.threadlist_tit', thread);
                const tid = thread_title_node.parentNode.href.parseURL().tid;
                if (tid <= last_tid) {
                    found = true;
                    break;
                }
                const title = qS('em', thread_title_node).textContent;
                const fid = qS('li.mr > a', thread).href.parseURL().fid;
                new_threads.push({ tid, title, fid });
                if (last_tid == 0) {
                    break;
                }
            }
        }
        last_tid = new_threads.length == 0 ? 1 : new_threads[0].tid;
        return { 'new': new_threads, found, 'last_tpid': last_tid }
    }

    async function getUserNewestPostInThread(uid, tid, last_pid = 0) {
        // 返回关注主题只看该作者末页(large_page_num)新于last_pid的回复,通过能否在末页查询到不晚于last_pid的回复判断是否可能有更多回复

        const URL_params = { loc: 'forum', mod: 'viewthread', tid, authorid: uid, page: large_page_num, mobile: '2' };
        const page_doc = await getPageDocInDomain(URL_params, mobileUA);
        const posts_in_page = getPostsInPage(page_doc);
        const thread_title = qS('meta[name="keywords"]', page_doc).content;
        let new_posts = [];
        let found = false;
        let pids = [];
        for (let i = posts_in_page.length - 1; i >= 0; i--) {
            const post = posts_in_page[i];
            const pid = getPostId(post);
            if (pid <= last_pid) {
                found = true;
                break;
            }
            pids.push(pid);
            if (last_pid == 0) {
                break;
            }
        }
        if (pids.length > 0) {
            new_posts.push({ tid, title: thread_title, pids });
        }
        last_pid = new_posts.length == 0 ? 1 : new_posts[0].pids[0]; // last_pid==0代表第一次查询新回复状态,所以完全没有回复的状态只能设为1
        return { new: new_posts, found, last_tpid: last_pid };
    }

    // ========================================================================================================
    // 关注、屏蔽与自动回复
    // ========================================================================================================
    // 根据checkbox的状态更新value对应的数组
    // value/id: tid/pid, uid/tid
    function recordCheckbox(value, id, checked) {
        let checked_list = GM_getValue(value, []);
        id = id.split('_check_')[1];
        updateListElements(checked_list, id, checked);
        updateGMList(value, checked_list);
    }

    // 关注某个用户在某个Thread下的回复
    // 若tid==0,则关注用户的所有主题
    // 若tid==-1, 则关注用户的所有回复
    function recordFollow(info, followed) {
        let followed_threads = GM_getValue(info.uid + '_followed_threads', []);
        updateListElements(followed_threads, { tid: info.tid, title: info.title, last_tpid: 0 }, followed, (a, b) => a.tid == b.tid); // last_tpid==0 表示这是新关注的用户
        updateGMList(info.uid + '_followed_threads', followed_threads);

        let followed_users = GM_getValue('followed_users', []);
        updateListElements(followed_users, { uid: info.uid, name: info.name }, followed_threads.length > 0, (a, b) => a.uid == b.uid);
        updateGMList('followed_users', followed_users);

        let followed_num = GM_getValue('followed_num', 0);
        followed_num += followed ? 1 : -1;
        followed_num = followed_num < 0 ? 0 : followed_num;
        GM.setValue('followed_num', followed_num);
    }

    function blockUser(uid, name) {
        hs.blacklist = updateListElements(hs.blacklist, { uid, name }, true, (a, b) => a.uid == b.uid);
        GM.setValue('helper_setting', hs);
        updateGMList(`${uid}_followed_threads`, []);
        const followed_users = GM_getValue('followed_users', []);
        updateListElements(followed_users, { uid, name }, false, (a, b) => a.uid == b.uid);
        updateGMList('followed_users', followed_users);
    }

    function autoReply(timeout = 2000) {
        const reply_text = hs.auto_reply_message;
        const reply_textarea = qS('#fastpostmessage');
        if (reply_textarea) {
            reply_textarea.value = reply_text;
        }
        const reply_btn = qS('#fastpostsubmit');
        if (reply_btn) {
            setTimeout(() => reply_btn.click(), timeout);
        }
    }


    // ========================================================================================================
    // 修改页面的工具
    // ========================================================================================================
    function setHidden(elem, hidden = true) {
        if (!elem) {
            return;
        }

        if (hidden) {
            elem.style.display = 'none';
        }
        else {
            elem.style.display = '';
        }
    }

    function insertElement(elem, pos, type = 'insertBefore') {
        switch (type) {
            case 'append':
                pos.appendChild(elem);
                break;
            case 'insertBefore':
                pos.parentNode.insertBefore(elem, pos);
                break;
            case 'insertAfter':
                if (pos.nextSibling) {
                    pos.parentNode.insertBefore(elem, pos.nextSibling);
                }
                else {
                    pos.parentNode.appendChild(elem);
                }
                break;
        }
    }

    function insertInteractiveLink(text, func, pos, type = 'append') {
        const a = docre('a');
        a.href = 'javascript:void(0)';
        a.textContent = text;
        if (func instanceof Function) {
            a.addEventListener('click', func);
        }
        insertElement(a, pos, type);
        return a;
    }

    function insertLink(text, URL_params, pos, max_text_length = 0, type = 'append') {
        const a = docre('a');
        if (max_text_length > 0 && text.length > max_text_length) {
            a.text = text.slice(0, max_text_length) + '...';
            a.title = text;
        }
        else {
            a.textContent = text;
        }
        a.href = createURLInDomain(URL_params);
        a.target = '_blank';
        insertElement(a, pos, type);
        return a;
    }

    function createFollowButton(info) {
        // info: { uid, name, tid, title }
        const follow_btn = docre('button');
        const follow_status = GM_getValue(info.uid + '_followed_threads', []);
        const followed = follow_status.some(e => e.tid == info.tid);
        follow_btn.type = 'button';
        follow_btn.className = 'helper-f-button';
        follow_btn.setAttribute('data-hfb-uid', info.uid);
        if (followed) {
            follow_btn.setAttribute('data-hfb-followed', '');
        }

        let follow_type = '';
        switch (info.tid) {
            case -1:
                follow_type = 'hfb-special';
                info.title = '所有回复';
                break;
            case 0:
                follow_type = 'hfb-normal';
                info.title = '所有主题';
                break;
            default:
                follow_type = 'hfb-thread';
        }
        follow_btn.classList.add(follow_type);

        follow_btn.addEventListener('click', async () => {
            const followed_num = GM_getValue('followed_num', 0);
            if (followed_num >= magic_num) {
                alert('关注数已达上限,请清理关注列表.');
                return;
            }
            const follow_status = GM_getValue(info.uid + '_followed_threads', []);
            const followed = follow_status.some(e => e.tid == info.tid);
            qSA(`.${follow_type}[data-hfb-uid='${info.uid}']`).forEach(e => {
                if (followed) {
                    e.removeAttribute('data-hfb-followed');
                }
                else {
                    e.setAttribute('data-hfb-followed', '')
                }
            });
            recordFollow(info, !followed);
            if (info.tid == -1 && !followed) { // 特关同时也关注主题
                recordFollow({ uid: info.uid, name: info.name, tid: 0, title: '所有主题' }, true);
            }
        });

        return follow_btn;
    }

    function addWrapInNode(root, min_wrap_length = hs.min_wrap_length, wrap_length = hs.typical_wrap_length, max_wrap_length = hs.max_wrap_length, dot_char = hs.wrap_dot, comma_char = hs.wrap_comma) {
        const findBreak = text => {
            for (let i = wrap_length; i < Math.min(text.length, max_wrap_length); i++) {
                if (dot_char.includes(text[i])) {
                    return i;
                }
            }
            if (text.length > max_wrap_length) {
                for (let i = max_wrap_length; i < text.length; i++) {
                    if (dot_char.includes(text[i]) || comma_char.includes(text[i])) {
                        return i;
                    }
                }
            }
            return -1;
        };

        let iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, null, false);
        let node = iter.nextNode();
        while (node) {
            let text = node.nodeValue;

            let break_index;
            if (text.length > wrap_length) {
                break_index = findBreak(text);
            }

            if (break_index > 0) {
                let text1 = text.slice(0, break_index + 1);
                let text2 = text.slice(break_index + 1);
                if (text2.trim().length > min_wrap_length) {
                    node.nodeValue = '';
                    let new_node1 = document.createTextNode(text1);
                    let br = docre('br');
                    br.setAttribute('data-hbr', 'auto-wrap');
                    let new_node2 = document.createTextNode(text2);
                    insertElement(new_node2, node, 'insertAfter');
                    insertElement(br, new_node2);
                    insertElement(new_node1, br);
                    let current_node = node;

                    node = iter.nextNode();
                    node = iter.nextNode();
                    node.parentNode.removeChild(current_node);
                    continue;
                }
            }
            node = iter.nextNode();
        }

        iter = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, { acceptNode: node => node.tagName == 'BR' }, false);
        node = iter.nextNode();
        while (node) {
            let in_multi_br = false;
            let last_in_multi_br = false;
            while (node) {
                if (node.nextSibling) {
                    const next = node.nextSibling;
                    const next_is_br = next.tagName == 'BR';
                    const next_is_space = next.nodeType == Node.TEXT_NODE && next.nodeValue.trim() == '';
                    const next_is_the_end = !next.nextSibling;
                    const nnext_is_br = next?.nextSibling?.tagName == 'BR';
                    const nnext_is_newline = next?.nextSibling?.nodeType == Node.TEXT_NODE && next?.nextSibling?.nodeValue.trim() != '';
                    const nnext_is_newblock = next?.nextSibling?.tagName == 'DIV';
                    in_multi_br = next_is_br || next_is_the_end || next_is_space && (nnext_is_br || nnext_is_newline || nnext_is_newblock);
                    if (in_multi_br) {
                        node = iter.nextNode();
                        last_in_multi_br = true;
                        continue;
                    }
                }
                break;
            }
            if (!in_multi_br && !last_in_multi_br) {
                const br = docre('br');
                br.setAttribute('data-hbr', 'before-single-br');
                insertElement(br, node);
            }
            node = iter.nextNode();
        }

        // 删掉引用中的自动<br>和引用后可能的第一个自动<br>
        qSA('div.quote', root).forEach(e => {
            const brs = qSA('br[data-hbr]', e);
            for (let br of brs) {
                br.parentNode.removeChild(br);
            }
            const next = e.nextElementSibling;
            if (next.getAttribute('data-hbr') == 'before-single-br') {
                next.parentNode.removeChild(next);
            }
        });
    }

    function removeWrapInNode(root) {
        let iter = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, {
            acceptNode: node => {
                if (node.tagName == 'BR' && node.hasAttribute('data-hbr') && node.getAttribute('data-hbr') == 'before-single-br') {
                    return NodeFilter.FILTER_ACCEPT;
                }
                return NodeFilter.FILTER_REJECT;
            }
        }, false);
        let node = iter.nextNode();

        while (node) {
            const current = node;
            node = iter.nextNode();
            current.parentNode.removeChild(current);
        }

        iter = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, {
            acceptNode: node => {
                if (node.tagName == 'BR' && node.hasAttribute('data-hbr') && node.getAttribute('data-hbr') == 'auto-wrap') {
                    return NodeFilter.FILTER_ACCEPT;
                }
                return NodeFilter.FILTER_REJECT;
            }
        }, false);
        node = iter.nextNode();

        while (node) {
            const next = node.nextSibling;
            const previous = node.previousSibling;
            const current = node;
            node = iter.nextNode();

            previous.nodeValue += next.nodeValue;
            current.parentNode.removeChild(next);
            current.parentNode.removeChild(current);
        }
    }

    // ========================================================================================================
    // 修改页面内容
    // ========================================================================================================
    function insertHelperLink() {
        let target_menu = qS('#myitem')
        if (target_menu) {
            const helper_setting_link = insertInteractiveLink('助手', () => { if (!qS('#helper-popup')) { createHelperPopup() } }, target_menu, 'insertBefore');
            helper_setting_link.id = 'helper_setting';
            const span = docre('span');
            span.textContent = ' | ';
            span.className = 'pipe';
            insertElement(span, target_menu);
            return;
        }

        target_menu = qS('#myspace');
        if (target_menu) {
            target_menu = qS('#myspace')
            insertInteractiveLink('助手', () => { if (!qS('#helper-popup')) { createHelperPopup() } }, target_menu, 'insertBefore');
            return;
        }
    }

    function modifyPostInPage() {
        const tid = location_params.tid;
        const posts_in_page = getPostsInPage();
        const thread_title = qS('#thread_subject').textContent;
        let all_checked = true;

        for (let post of posts_in_page) {
            const post_info = getPostInfo(post);
            const pid = post_info.post_id;
            const uid = post_info.post_auth_id;
            const name = post_info.post_auth;

            const label = docre('label');
            const checkbox = docre('input');
            checkbox.id = 'post_check_' + pid;
            checkbox.className = 'helper-checkbox';
            checkbox.type = 'checkbox';
            checkbox.addEventListener('change', () => { recordCheckbox(`${tid}_checked_posts`, checkbox.id, checkbox.checked) });// 每个Thread设置一个数组,存入被选中的Post的ID
            label.appendChild(checkbox);

            const label_text = document.createTextNode('保存本层');
            label.className = 'helper-checkbox-label o';
            label.appendChild(label_text);


            all_checked = all_checked && checkbox.checked;

            const user_card = qS('tbody > tr:nth-child(1) > td.pls > div', post)

            const profile_card = qS('[id^=userinfo] > div.i.y ', post);
            insertInteractiveLink('代表作', () => createMasterpiecePopup(uid, name), qS('div:first-child', profile_card));

            const post_follow_btn = createFollowButton({ uid, name, tid, title: thread_title });
            post_follow_btn.classList.add('o');
            user_card.appendChild(post_follow_btn);
            user_card.appendChild(label);

            const profile_icon = qS('div.imicn', profile_card);
            profile_icon.appendChild(createFollowButton({ uid, name, tid: 0 }));
            insertInteractiveLink(' 屏蔽用户', () => {
                blockUser(uid, name);
                updatePageDoc();
            }, profile_icon);
        }

        const label = docre('label');
        const label_text = document.createTextNode(all_checked ? '清空全选' : '全选本页');
        label.appendChild(label_text);
        qS('#postlist > table:nth-child(1) > tbody > tr > td.plc.ptm.pbn.vwthd > div').appendChild(label);

        const checkbox = docre('input');
        checkbox.id = 'page_checked_all';
        checkbox.type = 'checkbox';
        checkbox.className = 'helper-checkbox';
        checkbox.style.verticalAlign = 'middle';
        checkbox.checked = all_checked;
        checkbox.addEventListener('change', () => {
            qSA('[id^=post_check_]').forEach(e => e.checked = checkbox.checked);
            label_text.textContent = checkbox.checked ? '清空全选' : '全选本页';
        });
        label.appendChild(checkbox);
    }

    function modifyPostPage() {
        const the_only_author = theOnlyAuthorInfo();

        const saveFunc = (type = 'main') => () => {
            saveThread(type).then(() => {
                if (hs.enable_auto_reply) {
                    autoReply();
                }
            });
        };

        const down_first_link_pos = qS('#postlist > div > table > tbody > tr:nth-child(1) > td.plc > div.pi > strong');
        if (isFirstPage(location_params)) {
            insertInteractiveLink('保存主楼  ', saveFunc(), down_first_link_pos);
        }

        const down_thread_link_pos = qS('#postlist > table:nth-child(1) > tbody > tr > td.plc.ptm.pbn.vwthd > div');
        if (the_only_author) {
            insertInteractiveLink('保存作者  ', saveFunc('all'), down_thread_link_pos);
        }
        else {
            insertInteractiveLink('保存全帖  ', saveFunc('all'), down_thread_link_pos);
        }
        insertInteractiveLink('保存选中  ', saveFunc('checked'), down_thread_link_pos);

        modifyPostInPage();
    }

    function modifySpacePage() {
        const uid = location_params.uid;
        const toptb = qS('#toptb > div.z');
        if (toptb) {
            const name = getSpaceAuthor();
            insertLink(`${name}的主题`, { loc: 'home', mod: 'space', uid, do: 'thread', view: 'me', from: 'space' }, toptb);
            toptb.appendChild(createFollowButton({ uid, name, tid: location_params?.type == 'reply' ? -1 : 0 }));
            // updatePageDoc();
        }

        const addMergedownComponent = () => {
            const pos = qS('#delform > table > tbody > tr.th > th');
            const save_select = docre('select');
            const save_types = ['main', 'all', 'author'];
            const save_types_text = ['主楼(合并)', '全帖(打包)', '作者(打包)'];
            for (let i = 0; i < save_types.length; i++) {
                const option = docre('option');
                option.value = save_types[i];
                option.textContent = save_types_text[i];
                save_select.appendChild(option);
                if (save_types[i] == hs.default_merge_mode) {
                    option.selected = true;
                }
            }

            const save_link = insertInteractiveLink('  下载 ', () => saveMergedThreads(save_select.value), pos);
            save_select.addEventListener('change', () => {
                const new_save_link = save_link.cloneNode(true);
                new_save_link.addEventListener('click', () => {
                    saveMergedThreads(save_select.value);
                });
                save_link.replaceWith(new_save_link);
            });
            save_select.style.display = 'none';

            pos.appendChild(save_select);

            const thread_table = qS('#delform > table > tbody');
            if (qS('.emp', thread_table)) {
                return;
            }

            const thread_in_page = qSA('tr:not(.th)', thread_table);

            for (let thread of thread_in_page) {
                const link = qS('th > a', thread)
                const tid = link.href.parseURL().tid;
                const checkbox = docre('input');
                checkbox.id = 'thread_check_' + tid;
                checkbox.type = 'checkbox';
                checkbox.className = 'pc';

                insertElement(checkbox, link);

                if (qS('td:nth-child(3) > a', thread).textContent == '保密存档') {
                    checkbox.disabled = true;
                    continue;
                }

                checkbox.addEventListener('change', () => { recordCheckbox(`${uid}_checked_threads`, checkbox.id, checkbox.checked) });// 每个用户设置一个数组,存入被选中的thread的ID
            }
        };
        const addMasterpieceComponent = () => {
            const user_name = getSpaceAuthor();
            const header = document.querySelector('#ct > div.mn > div > div.bm_h > h1');
            const masterpiece = docre('span');
            masterpiece.className = 'xs1 xw0';
            const pipe = docre('span');
            pipe.className = 'pipe';
            pipe.textContent = '|';
            masterpiece.appendChild(pipe);
            insertInteractiveLink('代表作', () => createMasterpiecePopup(uid, user_name), masterpiece);
            header.appendChild(masterpiece);
        };
        const addDebugModeComponent = () => {
            const pos = qS('#pcd > div > ul');
            const label = docre('label');
            const checkbox = docre('input');
            checkbox.type = 'checkbox';
            checkbox.checked = hs.enable_debug_mode;
            checkbox.addEventListener('change', () => {
                hs.enable_debug_mode = checkbox.checked;
                hs.enable_debug_mode ? log.setLevel('debug') : log.resetLevel();
                log.info('调试模式已' + (hs.enable_debug_mode ? '开启' : '关闭'));
                GM.setValue('helper_setting', hs);
            });
            const text = document.createTextNode('调试模式');
            label.appendChild(checkbox);
            label.appendChild(text);
            pos.appendChild(label);
        };

        executeIfLoctionMatch(addMergedownComponent, { do: 'thread', type: ['thread', undefined] });
        executeIfLoctionMatch(addMasterpieceComponent, { do: 'thread', view: 'me', from: 'space' });
        executeIfLoctionMatch(addDebugModeComponent, { do: 'wall', uid: GM_info.script.author });
    }

    function modifyIndexPage() {
        setHidden(qS('#logo'));
        setHidden(qS('#desktop'));
        setHidden(qS('#mobile'));
        insertElement(createOverlay('redirect', () => { location.href = 'main' }), qS('#info'), 'insertBefore');
        log.log('Index page modified.');
    }

    function modifyPageDoc() {
        if (hasReadPermission() && isLogged()) {

            executeIfLoctionMatch(modifyIndexPage, { pathname: '/' },);
            executeIfLoctionMatch(modifyPostPage, { loc: 'forum', mod: 'viewthread', 'mobile': ['no', undefined] });
            executeIfLoctionMatch(modifySpacePage, { loc: 'home', mod: 'space', 'mobile': ['no', undefined] });

            updatePageDoc();

            if (hs.enable_notification) {
                updateNotificationPopup();
            }

            insertHelperLink();
        }
    }

    // ========================================================================================================
    // 更新页面内容
    // ========================================================================================================
    function updatePostsStaus() {
        const posts_in_page = getPostsInPage();
        const tid = location_params.tid;

        // 更新自动换行
        for (let post of posts_in_page) {
            if (hs.enable_auto_wrap) {
                const post_content = qS('[id^=postmessage]', post);
                addWrapInNode(post_content);
            }
            else {
                const post_content = qS('[id^=postmessage]', post);
                removeWrapInNode(post_content);
            }
        }

        // 更新“保存本层”复选框
        const checked_posts = GM_getValue(`${tid}_checked_posts`, []);
        qSA('[id^=post_check_]').forEach(e => {
            e.checked = checked_posts.includes(e.id.slice(11));
        });
    }

    function updateForumStaus() {
        qSA('[id^=normalthread]').forEach(thread => {
            const title = qS('a.s.xst', thread).innerText.trim().toLowerCase();
            const uid = qS('td.by cite a', thread).href.parseURL().uid;
            // 屏蔽关键词
            if (hs.enable_block_keyword && hs.block_keywords.some(keyword => title.includes(keyword))) {
                thread.style.display = 'none';
            }
            // 屏蔽用户
            else if (hs.enable_blacklist && hs.blacklist.some(e => e.uid == uid)) {
                thread.style.display = 'none';
            }
            else {
                thread.style.display = '';
            }
        });
    }

    async function updateMergedownStaus() {
        // 更新多选下载复选框
        const checked_threads = await GM.getValue(location_params.uid + '_checked_threads', []);
        qSA('[id^=thread_check_]').forEach(e => {
            e.checked = checked_threads.includes(e.id.slice(13));
        });
    }

    function updateFollowOrBlockButtonsStatus() {
        for (let { uid } of hs.blacklist) {
            qSA(`.helper-f-button[data-hfb-uid='${uid}']`).forEach(e => {
                e.removeAttribute('data-hfb-followed');
                e.classList.remove('helper-f-button');
                e.classList.add('helper-b-button');
                e.replaceWith(e.cloneNode(true));
            });
        }

        qSA('.helper-b-button').forEach(e => {
            if (!hs.blacklist.some(p => p.uid == e.getAttribute('data-hfb-uid'))) {
                e.classList.remove('helper-b-button');
                e.classList.add('helper-f-button');
                e.addEventListener('click', async () => {
                    const followed_num = GM_getValue('followed_num', 0);
                    if (followed_num >= magic_num) {
                        alert('关注数已达上限,请清理关注列表.');
                        return;
                    }
                    const follow_status = GM_getValue(info.uid + '_followed_threads', []);
                    const followed = follow_status.some(e => e.tid == info.tid);
                    qSA(`.${follow_type}[data-hfb-uid='${info.uid}']`).forEach(e => {
                        if (followed) {
                            e.removeAttribute('data-hfb-followed');
                        }
                        else {
                            e.setAttribute('data-hfb-followed', '')
                        }
                    });
                    recordFollow(info, !followed);
                    if (info.tid == -1 && !followed) { // 特关同时也关注主题
                        recordFollow({ uid: info.uid, name: info.name, tid: 0, title: '所有主题' }, true);
                    }
                });
            }
        });
    }

    function updatePageDoc() {
        executeIfLoctionMatch(updatePostsStaus, { loc: 'forum', mod: 'viewthread', 'mobile': ['no', undefined] });
        executeIfLoctionMatch(updateForumStaus, { loc: 'forum', mod: 'forumdisplay', 'mobile': ['no', undefined] });
        executeIfLoctionMatch(updateMergedownStaus, { loc: 'home', mod: 'space', do: 'thread', type: ['thread', undefined], 'mobile': ['no', undefined] });
        updateFollowOrBlockButtonsStatus();
    }

    // ========================================================================================================
    // 浮动弹窗通用工具
    // ========================================================================================================
    function removeHelperPopup() {
        const popup = qS('#helper-popup');
        if (popup) {
            document.body.removeChild(popup);
        }
        const overlay = qS('#helper-overlay');
        if (overlay) {
            document.body.removeChild(overlay);
        }
    }

    function createCloseButton(onclick) {
        const close_btn = docre('button');
        close_btn.className = 'helper-close-btn';
        close_btn.type = 'button';
        close_btn.addEventListener('click', onclick);
        return close_btn;
    }

    function createOverlay(type = 'shield', onclick = removeHelperPopup) {
        const overlay = docre('div');
        overlay.id = 'helper-overlay';
        overlay.className = 'helper-' + type + '-layer';
        overlay.addEventListener('click', onclick);
        return overlay;
    }

    function createLoadingOverlay() {
        const overlay = docre('div');
        overlay.id = 'helper-loading-overlay';
        const spiner = docre('div');
        spiner.className = 'helper-spinner';
        overlay.appendChild(spiner);
        return overlay;
    }

    function createPopupWithTitle(title) {
        const popup = docre('div');
        const overlay = createOverlay();
        popup.id = 'helper-popup';

        const helper_title_container = docre('div');
        helper_title_container.id = 'helper-title-container';
        popup.appendChild(helper_title_container);

        const helper_title = docre('div');
        helper_title.id = 'helper-title';
        helper_title.textContent = title;
        helper_title_container.appendChild(helper_title);

        const close_btn = createCloseButton(removeHelperPopup);
        helper_title_container.appendChild(close_btn);

        const hr = docre('hr');
        hr.className = 'helper-hr';
        popup.appendChild(hr);

        const content_container = docre('div');
        content_container.id = 'helper-content-container';
        popup.appendChild(content_container);

        document.body.appendChild(overlay);
        return popup;
    }

    function createCenterMessageDiv(message) {
        const div = docre('div');
        div.className = 'helper-center-message';
        div.textContent = message;
        return div;
    }

    function createTopProgressbar() {
        const container = docre('div');
        container.id = 'helper-top-progressbar-container';
        const progressbar = docre('div');
        progressbar.id = 'helper-top-progressbar';
        container.appendChild(progressbar);

        document.body.appendChild(container);
        return progressbar;
    }

    function updateProgressbar(progress, dt, remove_timeout = 1500) {
        if (progress && dt && dt > 0) {
            progress.value += dt;
            progress.bar.style.width = progress.value + '%';

            if (progress.value >= 100) {
                setTimeout(() => {
                    document.body.removeChild(progress.bar.parentNode);
                }, remove_timeout);
            }
        }
    }

    // ========================================================================================================
    // 助手设置组件
    // ========================================================================================================
    function createHelperSettingSelect(attr, options = [], texts = []) {
        const status = hs[attr];
        if (options.length == 0) {
            options = [status];
        }

        const select = docre('select');
        select.className = 'helper-select helper-active-component';
        options.forEach(option => {
            const opt = docre('option');
            opt.value = option;
            opt.textContent = texts[options.indexOf(option)] || option;
            if (option == status) {
                opt.selected = true;
            }
            select.appendChild(opt);
        });

        select.addEventListener('change', (e) => {
            hs[attr] = e.target.value;
            GM.setValue('helper_setting', hs);
        });

        return select;
    }

    function createHelperSettingSwitch(attr, onchange = null) {
        const label = docre('label');

        const checkbox = docre('input');
        checkbox.type = 'checkbox';
        checkbox.checked = hs[attr];
        checkbox.addEventListener('change', (e) => {
            hs[attr] = e.target.checked;
            GM.setValue('helper_setting', hs);
            if (onchange) {
                onchange();
            }
        });
        label.appendChild(checkbox);

        const span = docre('span');
        span.className = 'helper-toggle-switch helper-halfheight-active-component';
        label.appendChild(span);

        return label;
    }

    function createHelperSettingMultiCheck(multichecks) {
        const container = docre('div');
        container.className = 'helper-multicheck-container helper-active-component';

        multichecks.forEach(option => {
            const item = docre('div');
            item.className = 'helper-multicheck-item';

            const checkbox = docre('input');
            checkbox.type = 'checkbox';
            checkbox.checked = hs[option.attr];
            checkbox.addEventListener('change', (e) => {
                hs[option.attr] = e.target.checked;
                GM.setValue('helper_setting', hs);
            });
            item.appendChild(checkbox);

            const item_text = docre('div');
            item_text.textContent = option.text;
            item_text.className = 'helper-multicheck-text';
            item_text.addEventListener('click', () => {
                checkbox.checked = !checkbox.checked;
                checkbox.dispatchEvent(new Event('change'));
            });
            item.appendChild(item_text);

            container.appendChild(item);
        });

        return container;
    }

    function createHelperSettingButton(btn_text, onclick) {
        const btn = docre('button');
        btn.type = 'button';
        btn.className = 'helper-setting-button helper-active-component';
        btn.textContent = btn_text;
        btn.addEventListener('click', onclick);

        return btn;
    }

    function createHelperActiveComponent(type, args) {
        switch (type) {
            case 'switch':
                return createHelperSettingSwitch(...args);
            case 'multicheck':
                return createHelperSettingMultiCheck(...args);
            case 'select':
                return createHelperSettingSelect(...args);
            case 'button':
                return createHelperSettingButton(...args);
            default:
                return docre('div');
        }
    }

    function createHeperTagsContainer(tag_list, onremove = null, value_attr = null) {
        const tag_container = docre('div');
        tag_container.className = 'helper-tag-container';

        const renderTags = () => {
            tag_container.innerHTML = '';
            tag_list.forEach((tag_info, index) => {
                const tag = docre('div');
                tag.className = 'helper-tag';
                if (value_attr && (value_attr in tag_info)) {
                    tag.textContent = tag_info[value_attr];
                }
                else {
                    tag.textContent = tag_info;
                }

                const remove_btn = docre('button');
                remove_btn.type = 'button';
                remove_btn.className = 'helper-tag-remove-btn';
                remove_btn.textContent = '×';
                remove_btn.addEventListener('click', () => {
                    tag_list.splice(index, 1);
                    renderTags();
                    updatePageDoc();
                    if (onremove) {
                        onremove();
                    }
                });

                tag.appendChild(remove_btn);
                tag_container.appendChild(tag);
            });
        };
        renderTags();

        return tag_container;
    }

    // ========================================================================================================
    // 助手设置弹窗
    // ========================================================================================================
    function createFollowListTab() {
        const followed_users = GM_getValue('followed_users', []);

        if (followed_users.length == 0) {
            return createCenterMessageDiv('暂无关注');
        }

        const table = docre('table');
        table.className = 'helper-popup-table';
        const table_head = docre('thead');
        table.appendChild(table_head);
        const title_row = table_head.insertRow();

        const user_title = docre('th');
        const thread_title = docre('th');
        const follow_title = docre('th');
        user_title.textContent = '用户';
        thread_title.textContent = '关注内容';
        follow_title.textContent = '操作';
        [user_title, thread_title, follow_title].forEach(e => {
            title_row.appendChild(e);
            e.className = 'helper-sticky-header';
        });

        const table_body = docre('tbody');
        table.appendChild(table_body);

        for (let user of followed_users) {
            const followed_threads = GM_getValue(user.uid + '_followed_threads', []);
            const user_URL_params = { loc: 'home', mod: 'space', uid: user.uid };

            if (followed_threads.some(e => e.tid == -1)) {
                const row = table_body.insertRow();
                const [user_cell, thread_cell, follow_cell] = [0, 1, 2].map(i => row.insertCell(i));

                insertLink(user.name, user_URL_params, user_cell);
                const thread_URL_params = { loc: 'home', mod: 'space', uid: user.uid, do: 'thread', view: 'me', type: 'reply', from: 'space' };
                insertLink('所有回复', thread_URL_params, thread_cell);
                follow_cell.appendChild(createFollowButton({ 'uid': user.uid, name: user.name, tid: -1, title: '所有回复' }));
                continue;
            }

            for (let thread of followed_threads) {
                const row = table_body.insertRow();
                const [user_cell, thread_cell, follow_cell] = [0, 1, 2].map(i => row.insertCell(i));

                insertLink(user.name, user_URL_params, user_cell);
                let thread_URL_params;
                if (thread.tid > 0) {
                    thread_URL_params = { loc: 'forum', mod: 'viewthread', tid: thread.tid };
                }
                else if (thread.tid == 0) {
                    thread_URL_params = { loc: 'home', mod: 'space', uid: user.uid, do: 'thread', view: 'me', from: 'space' };
                }

                insertLink(thread.title, thread_URL_params, thread_cell);
                follow_cell.appendChild(createFollowButton({ uid: user.uid, name: user.name, tid: thread.tid, title: thread.title }));
            }
        }
        return table;
    }

    function createHistoryNotificationTab() {
        const notification_messages = GM_getValue('notification_messages', []);

        if (notification_messages.length == 0) {
            return createCenterMessageDiv('暂无历史通知');
        }

        const div = docre('div');
        notification_messages.forEach(message => { div.innerHTML += message; });
        return div;
    }

    function createBlockKeywordTab() {
        const div = docre('div');
        const title_div = docre('div');
        title_div.className = 'helper-sticky-header';
        const input = docre('input');
        input.type = 'text';
        input.placeholder = '输入屏蔽词并回车提交';
        input.className = 'helper-input';

        let tag_container = createHeperTagsContainer(hs.block_keywords, () => GM.setValue('helper_setting', hs));
        input.addEventListener('keyup', e => {
            if (e.key == 'Enter') {
                const keyword = input.value.trim().toLowerCase();
                if (keyword.length > 0 && !hs.block_keywords.includes(keyword)) {
                    hs.block_keywords.push(keyword);
                    GM.setValue('helper_setting', hs);
                    div.removeChild(tag_container);
                    tag_container = createHeperTagsContainer(hs.block_keywords, () => GM.setValue('helper_setting', hs));
                    div.appendChild(tag_container);
                    updatePageDoc();
                }
                input.value = '';
            }
        });

        div.appendChild(input);
        div.appendChild(tag_container);

        return div;
    }

    function createBlacklistTab() {
        return createHeperTagsContainer(hs.blacklist, () => GM.setValue('helper_setting', hs), 'name');
    }

    function createDebugTab() {
        const div = docre('div');
        const all_value = GM_listValues();
        all_value.forEach(element => {
            const p = docre('p');
            p.textContent = element;
            p.addEventListener('click', () => {
                div.innerHTML = '';
                div.textContent = element + ':' + JSON.stringify(GM_getValue(element));
            });
            div.appendChild(p);
        });
        return div;
    }

    function createHelperSettingTab(setting_type) {
        const div = docre('div');
        let components = [];

        switch (setting_type) {
            case 'read':
                components = [
                    // 开启更新通知
                    { title: '订阅更新通知', type: 'switch', args: ['enable_notification', () => setHidden(qS('#htb-follow'), !hs.enable_notification)] },
                    // 开启历史消息
                    { title: '保存历史通知', type: 'switch', args: ['enable_history', () => setHidden(qS('#htb-history'), !hs.enable_history)] },
                    // 开启辅助换行
                    { title: '自动换行', type: 'switch', args: ['enable_auto_wrap', updatePageDoc] },
                    // 开启屏蔽词
                    {
                        title: '标题关键词屏蔽', type: 'switch', args: ['enable_block_keyword', () => {
                            updatePageDoc();
                            setHidden(qS('#htb-block'), !hs.enable_block_keyword);
                        }
                        ]
                    },
                    // 开启黑名单
                    {
                        title: '黑名单', type: 'switch', args: ['enable_blacklist', () => {
                            updatePageDoc();
                            setHidden(qS('#htb-blacklist'), !hs.enable_blacklist);
                        }]
                    }];
                break;
            case 'save':
                components = [
                    // 选择下载内容
                    {
                        title: '保存帖子内容', type: 'multicheck', args: [[
                            { attr: 'enable_text_download', text: '文本' },
                            { attr: 'enable_postfile_download', text: '帖内资源' },
                            { attr: 'enable_attach_download', text: '附件' },
                            { attr: 'enable_op_download', text: '原创资源' }]]
                    },
                    // 选择文件打包模式
                    {
                        title: '帖内打包保存方式', type: 'select', args: [
                            'files_pack_mode',
                            ['no', 'single', 'all'],
                            ['不打包', '分类打包', '全部打包']]
                    },

                    // 开启自动回复
                    {
                        title: '保存后自动回复', type: 'switch', args: ['enable_auto_reply']
                    },
                    // 选择默认合并下载模式
                    {
                        title: '空间合并保存方式', type: 'select', args: [
                            'default_merge_mode',
                            ['main', 'author', 'all'],
                            ['主楼(合并)', '作者(打包)', '全帖(打包)']]
                    }
                ];
                break;
            case 'data':
                components = [
                    // 清除历史消息
                    {
                        title: '清空历史通知', type: 'button', args: ['全部清空', () => {
                            const confirm = window.confirm('确定清空所有历史通知?');
                            if (confirm) {
                                GM.deleteValue('notification_messages');
                                location.reload();
                            }
                        }]
                        , hidden: !hs.enable_history
                    },

                    // 清除脚本数据
                    {
                        title: '清空脚本数据', type: 'button', args: ['全部清空', () => {
                            const confirm = window.confirm('确定清空脚本所有数据?');
                            if (confirm) {
                                GM_listValues().forEach(e => GM.deleteValue(e));
                                location.reload();
                            }
                        }]
                    }];
                break;
        }

        components.forEach(component => {
            const container = docre('div');
            container.className = 'helper-setting-container';

            const text_node = docre('div');
            text_node.textContent = component.title;
            container.appendChild(text_node);

            const active_component = createHelperActiveComponent(component.type, component.args);
            container.appendChild(active_component);

            if (!component.hasOwnProperty('hidden')) {
                component.hidden = false;
            }
            setHidden(container, component.hidden);
            div.appendChild(container);
        });

        return div;
    }

    function createHelperPopup() {
        const popup = createPopupWithTitle('湿热助手');

        const content_container = qS('#helper-content-container', popup);

        const tab_btn_container = docre('div');
        tab_btn_container.id = 'helper-tab-btn-container';
        tab_btn_container.className = 'helper-scroll-component';
        content_container.appendChild(tab_btn_container);

        const tab_content_container = docre('div');
        tab_content_container.id = 'helper-tab-content-container';
        tab_content_container.className = 'helper-scroll-component';
        content_container.appendChild(tab_content_container);

        const tabs = [
            { id: 'view', name: '浏览设置', func: () => createHelperSettingTab('read') },
            { id: 'save', name: '下载设置', func: () => createHelperSettingTab('save') },
            { id: 'data', name: '数据设置', func: () => createHelperSettingTab('data') },
            { id: 'follow', name: '关注列表', func: createFollowListTab, 'hidden': !hs.enable_notification },
            { id: 'history', name: '历史提醒', func: createHistoryNotificationTab, 'hidden': !hs.enable_history },
            { id: 'block', name: '标题屏蔽词', func: createBlockKeywordTab, 'hidden': !hs.enable_block_keyword },
            { id: 'blacklist', name: '黑名单', func: createBlacklistTab, 'hidden': !hs.enable_blacklist },
            { id: 'debug', name: '调试', func: createDebugTab, hidden: !hs.enable_debug_mode }];

        const show_tab = content => {
            tab_content_container.innerHTML = '';
            tab_content_container.appendChild(content)
        };

        tabs.forEach((tab, index) => {
            const btn = docre('button');
            btn.type = 'button';
            btn.id = 'htb-' + tab.id;
            btn.className = 'helper-tab-btn';
            btn.textContent = tab.name;

            btn.addEventListener('click', () => {
                qSA('button', tab_btn_container).forEach(e => e.classList.remove('helper-tab-selected'));
                btn.classList.add('helper-tab-selected');
                show_tab(tab.func());
            });

            if (index == 0) {
                btn.classList.add('helper-tab-selected');
                show_tab(tab.func());
            }

            if (!tab.hasOwnProperty('hidden')) {
                tab.hidden = false;
            }
            setHidden(btn, tab.hidden);
            tab_btn_container.appendChild(btn);
        });

        document.body.appendChild(popup);
    }

    // ========================================================================================================
    // 消息提醒弹窗
    // ========================================================================================================

    function createNotificationPopup() {
        const popup = docre('div');
        popup.id = 'helper-notification-popup';
        document.body.appendChild(popup);

        const close_btn = createCloseButton(() => { popup.style.display = 'none' });
        close_btn.style.position = 'absolute';
        close_btn.style.top = '10px';
        close_btn.style.right = '10px';
        close_btn.classList.add('helper-redx');
        popup.appendChild(close_btn);
    }

    async function updateNotificationPopup() {
        const followed_users = await GM.getValue('followed_users', []);
        if (followed_users.length > 0) {
            let popup = qS('#helper-notification-popup');
            let notification_messages = [];
            let promises = [];

            const createParaAndInsertUserNameLink = (user, parent) => {
                const messageElement = docre('div');
                messageElement.className = 'helper-noti-message';
                parent.appendChild(messageElement);
                const user_URL_params = { loc: 'home', mod: 'space', uid: user.uid };
                const user_link = insertLink(user.name, user_URL_params, messageElement);
                user_link.className = 'helper-ellip-link';
                user_link.style.maxWidth = '30%';
                user_link.style.color = 'inherit !important';
                return messageElement;
            }
            const processNewInfos = (user, thread, new_infos) => {
                let followed_threads = GM_getValue(user.uid + '_followed_threads', []);
                let new_threads = new_infos.new;
                const found_last = new_infos.found;
                const last_tpid = new_infos.last_tpid;

                if (new_threads.length > 0) {
                    updateListElements(followed_threads, { tid: thread.tid, last_tpid, title: thread.title }, true, (a, b) => a.tid == b.tid);
                    updateGMList(user.uid + '_followed_threads', followed_threads);
                }

                if (thread.last_tpid == 0 || new_threads.length == 0) { // 如果没有更新,或者是首次关注,则不发送消息
                    return notification_messages;
                }

                if (!popup) {
                    createNotificationPopup();
                    popup = qS('#helper-notification-popup');
                }

                const div = docre('div');
                if (thread.tid != 0) {
                    for (let new_thread of new_threads) {
                        const thread_title = new_thread.title;
                        const messageElement = createParaAndInsertUserNameLink(user, div);
                        let message = ` 有`;
                        if (!found_last && thread.tid != -1) { // 在特定关注主题末页未找到不晚于last_pid的
                            message += '至少';
                        }
                        message += `${new_thread.pids.length}条新回复在 `;
                        const text_element = document.createTextNode(message);
                        messageElement.appendChild(text_element);
                        const thread_URL_params = { loc: 'forum', mod: 'redirect', goto: 'findpost', ptid: new_thread.tid, pid: new_thread.pids.at(-1) };
                        const thread_message = insertLink(thread_title, thread_URL_params, messageElement);
                        thread_message.className = 'helper-ellip-link';
                    }
                    if (!found_last && thread.tid == -1) { // 在空间回复页首页未找到不晚于last_pid的
                        const messageElement = createParaAndInsertUserNameLink(user, div);
                        const text_element2 = document.createTextNode(' 或有 ');
                        messageElement.appendChild(text_element2);
                        const reply_URL_params = { loc: 'home', mod: 'space', 'uid': user.uid, do: 'thread', view: 'me', type: 'reply', from: 'space' };
                        insertLink('更多新回复', reply_URL_params, messageElement);
                    }
                }
                else if (thread.tid == 0) {
                    const noti_threads = new_threads.filter(e => hs.important_fids.includes(e.fid));
                    hs.important_fids.forEach(fid => { new_threads = new_threads.filter(e => e.fid != fid) });
                    const notif_num = new_threads.length > hs.max_noti_threads ? hs.max_noti_threads : new_threads.length;
                    noti_threads.push(...new_threads.slice(0, notif_num));
                    for (let new_thread of noti_threads) {
                        const messageElement = createParaAndInsertUserNameLink(user, div);
                        const text_element = document.createTextNode(' 有新帖 ');
                        messageElement.appendChild(text_element);
                        const thread_URL_params = { loc: 'forum', mod: 'viewthread', tid: new_thread.tid };
                        const thread_message = insertLink(new_thread.title, thread_URL_params, messageElement);
                        thread_message.className = 'helper-ellip-link';
                        if (hs.important_fids.includes(new_thread.fid)) {
                            thread_message.style = 'color: red !important';
                        }
                    }
                    if (new_threads.length > 3) {
                        const messageElement = createParaAndInsertUserNameLink(user, div);
                        let message = ` 有另外 `;
                        if (!found_last) {
                            message += '至少';
                        }
                        const text_element = document.createTextNode(message);
                        messageElement.appendChild(text_element);
                        const thread_URL_params = { loc: 'home', mod: 'space', 'uid': user.uid, do: 'thread', view: 'me', from: 'space' };
                        insertLink(`${new_threads.length - 3}条新帖`, thread_URL_params, messageElement);
                    }
                }
                popup.appendChild(div);
                notification_messages.push(div.innerHTML);

                return notification_messages;
            };

            for (let user of followed_users) {
                let followed_threads = GM_getValue(user.uid + '_followed_threads', []);
                for (let thread of followed_threads) {
                    promises.push(
                        getUserNewestPostOrThread(user.uid, thread.tid, thread.last_tpid).then(
                            new_infos => processNewInfos(user, thread, new_infos)
                        )
                    );
                }
            }
            if (hs.enable_history) {
                await Promise.all(promises);
                const old_notification_messages = GM_getValue('notification_messages', []);
                notification_messages.push(...old_notification_messages);
                updateGMList('notification_messages', notification_messages);
            }
        }
    }

    // ========================================================================================================
    // 代表作弹窗
    // ========================================================================================================
    function createMasterpieceTable(masterpiece_info, sortby = 'view') {
        const div = docre('div');
        div.className = 'helper-scroll-component';
        div.style.width = '100%';
        div.style.paddingBottom = '10px';

        const has_thread = ['view', 'reply'].some(e => masterpiece_info['max_' + e + '_threads'].length > 0);
        if (!has_thread) {
            div.appendChild(createCenterMessageDiv('暂无作品'));
            return div;
        }

        let masterpiece_list = [];
        const updateTable = sort_by => {
            const table = docre('table');
            table.className = 'helper-popup-table';
            const table_head = docre('thead');
            table.appendChild(table_head);
            const title_row = table_head.insertRow();

            const thread_title = docre('th');
            const view_title = docre('th');
            const reply_title = docre('th');
            thread_title.textContent = '主题';
            view_title.textContent = '浏览';
            reply_title.textContent = '回复';
            switch (sort_by) {
                case 'reply':
                    reply_title.className = 'helper-sortby';
                    masterpiece_list = masterpiece_info.max_reply_threads;
                    view_title.addEventListener('click', () => {
                        div.innerHTML = '';
                        updateTable('view');
                    });
                    view_title.style.cursor = 'pointer';
                    break;
                case 'view':
                default:
                    view_title.className = 'helper-sortby';
                    masterpiece_list = masterpiece_info.max_view_threads;
                    reply_title.addEventListener('click', () => {
                        div.innerHTML = '';
                        updateTable('reply');
                    });
                    reply_title.style.cursor = 'pointer';
            }

            [thread_title, view_title, reply_title].forEach(e => title_row.appendChild(e));

            const table_body = docre('tbody');
            table.appendChild(table_body);


            for (let thread of masterpiece_list) {
                const row = table_body.insertRow();
                const [thread_cell, view_cell, reply_cell] = [0, 1, 2].map(i => row.insertCell(i));

                const thread_URL_params = { loc: 'forum', mod: 'viewthread', tid: thread.tid };
                insertLink(thread.title, thread_URL_params, thread_cell);
                thread_cell.innerHTML = (thread.spanHTML ?? '') + thread_cell.innerHTML;
                view_cell.textContent = thread.views;
                reply_cell.textContent = thread.replies;
            }
            div.appendChild(table);
        };

        updateTable();

        return div;
    }

    async function createMasterpiecePopup(uid, user_name) {
        const popup = createPopupWithTitle(`${user_name} 的代表作`);
        document.body.appendChild(popup);
        const content_container = qS('#helper-content-container', popup);

        let masterpiece_info = GM_getValue(uid + '_masterpiece', { update_time: 0, max_view_threads: [], max_reply_threads: [] });
        const loadContent = async () => {
            content_container.appendChild(createLoadingOverlay());
            const top_progressbar = createTopProgressbar();
            let progress = { value: 0, bar: top_progressbar };
            masterpiece_info = await updateMasterpiece(uid, progress);
            content_container.innerHTML = '';
        };

        if (Date.now() - masterpiece_info.update_time > hs.data_cache_time) {
            await loadContent();
        }
        content_container.appendChild(createMasterpieceTable(masterpiece_info, hs.default_masterpiece_sort));

        const updateFootnote = () => {
            const footnote = docre('div');
            content_container.appendChild(footnote);
            footnote.className = 'helper-footnote';
            const update_time_ago = timeAgo(masterpiece_info.update_time);
            footnote.textContent = `缓存时间:${update_time_ago}`;
            if (update_time_ago != '今天内' || hs.enable_debug_mode) {
                footnote.textContent += ' | ';
                const reload_link = insertInteractiveLink('立即刷新', async () => {
                    await loadContent();
                    content_container.appendChild(createMasterpieceTable(masterpiece_info, hs.default_masterpiece_sort));
                    updateFootnote();
                }, footnote);
                reload_link.style.color = 'inherit !important';
            }
        };

        updateFootnote();
    }

    async function updateMasterpiece(uid, progress = null) {
        const getUserThreadNum = async (uid) => {
            const URL_params = { loc: 'home', mod: 'space', uid: uid, do: 'profile' };
            const page_doc = await getPageDocInDomain(URL_params);
            const thread_info = qS('#ct > div.mn > div > div.bm_c > div > div:nth-child(1) > ul.cl.bbda.pbm.mbm > li > a:nth-child(12)', page_doc);
            const thread_num = Number(thread_info.textContent.slice(4));
            return thread_num > 0 ? thread_num : 1;
        };

        const max_page = Math.ceil(await getUserThreadNum(uid) / 20);
        updateProgressbar(progress, 10);
        const dt = decimalCeil(90 / max_page);

        let threads = [];
        let promises = Array.from({ length: max_page }, (v, k) => k + 1).map(page_num => new Promise(async (resolve, reject) => {
            const URL_params = { loc: 'home', mod: 'space', 'uid': uid, do: 'thread', view: 'me', page: page_num, mobile: '2' };
            const page_doc = await getPageDocInDomain(URL_params, mobileUA);
            const threads_in_page = qSA('li.list', page_doc);
            for (let thread of threads_in_page) {
                const title_node = qS('.threadlist_tit', thread);
                const spanHTML = qS('span', title_node)?.outerHTML;
                const title = qS('em', title_node).textContent;
                const tid = title_node.parentNode.href.parseURL().tid;
                const views = Number(qS('.dm-eye-fill', thread).nextSibling.textContent);
                const replies = Number(qS('.dm-chat-s-fill', thread).nextSibling.textContent);
                threads.push({ tid, spanHTML, title, views, replies });
            }
            updateProgressbar(progress, dt);
            resolve();
        }));
        await Promise.all(promises);

        const max_view_threads = threads.sort((a, b) => b.views - a.views).slice(0, hs.masterpiece_num);
        const max_reply_threads = threads.sort((a, b) => b.replies - a.replies).slice(0, hs.masterpiece_num);
        const masterpiece_info = { update_time: Date.now(), max_view_threads, max_reply_threads };
        GM.setValue(uid + '_masterpiece', masterpiece_info);
        return masterpiece_info;
    }

    // ========================================================================================================
    // 插入表情
    // ========================================================================================================
    async function modifySmiliesArray(new_smilies) {
        await checkVariableDefined('smilies_array');
        for (let smilies of new_smilies) {
            smilies_type['_' + smilies.type] = [smilies.name, smilies.path];
            smilies_array[smilies.type] = new Array();
            smilies_array[smilies.type][1] = smilies.info;
        }
    }

    async function modifySmiliesSwitch(original_smilies_types, mode = 'img') {
        await checkVariableDefined('smilies_switch');
        let smilies_switch_str = unsafeWindow['smilies_switch'].toString();
        smilies_switch_str = smilies_switch_str.replace("STATICURL+'image/smiley/'+smilies_type['_'+type][1]+'/'", `('${original_smilies_types}'.split(',').includes(type.toString())?(STATICURL+'image/smiley/'+smilies_type['_'+type][1]+'/'):smilies_type['_'+type][1])`);
        if (mode == 'img') {
            // TODO fastpost时有问题
            smilies_switch_str = smilies_switch_str.replace("'insertSmiley('+s[0]+')'", `"insertText('[img]"+smilieimg+"[/img]',strlen('[img]"+smilieimg+"[/img]'),0)"`);
        }
        smilies_switch = new Function('return ' + smilies_switch_str)();
    }

    async function insertExtraSmilies(id, seditorkey, original_smilies_types, new_smilies) {
        await modifySmiliesArray(new_smilies);
        await modifySmiliesSwitch(original_smilies_types, 'img');
        smilies_show(id, 8, seditorkey);
    }

    async function modifyBBCode2Html(original_smilies_types) {
        // 可以正常使用,但由于modifyPostOnSubmit的缘故,同步弃用
        await checkVariableDefined('bbcode2html');
        let bbcode2html_str = unsafeWindow['bbcode2html'].toString();
        bbcode2html_str = bbcode2html_str.replace("STATICURL+'image/smiley/'+smilies_type['_'+typeid][1]+'/'", `('${original_smilies_types}'.split(',').includes(typeid.toString())?(STATICURL+'image/smiley/'+smilies_type['_'+typeid][1]+'/'):smilies_type['_'+typeid][1])`);
        bbcode2html_str = bbcode2html_str.replace("}if(!fetchCheckbox('bbcodeoff')&&allowbbcode){", "}if(!fetchCheckbox('bbcodeoff')&&allowbbcode){")
        bbcode2html = new Function('return ' + bbcode2html_str)();
    }

    async function modifyPostOnSubmit(submit_id, original_smilies_types) {
        // TODO 不知道为什么,不这么做的话自定义表情在提交时会被转义成bbcode
        // TODO 但是对于fastpost的情况还是没法处理,所以暂时弃用
        const post = qS('#' + submit_id);
        submit_id = submit_id.replace('form', '');
        // const original_onsubmit_str = post.getAttribute('onsubmit').toString();
        post.setAttribute('onsubmit', `if(typeof smilies_type == 'object'){for (var typeid in smilies_array){for (var page in smilies_array[typeid]){for(var i in smilies_array[typeid][page]){re=new RegExp(preg_quote(smilies_array[typeid][page][i][1]),"g");this.message.value=this.message.value.replace(re,'[img]'+('${original_smilies_types}'.split(',').includes(typeid.toString())?(STATICURL+'image/smiley/'+ smilies_type['_' + typeid][1] + '/'):smilies_type['_' + typeid][1])+smilies_array[typeid][page][i][2]+"[/img]");}}}}`);
    }

    // ========================================================================================================
    // 窗口事件和DOM属性
    // ========================================================================================================
    window.addEventListener('keydown', e => {
        if (e.key == 'Escape') {
            const noti_popup = qS('#helper-notification-popup');
            if (noti_popup && noti_popup.style.display != 'none') {
                noti_popup.style.display = 'none';
            }
            else {
                removeHelperPopup();
            }
        }
    });

    document.original_url = document.URL;

    // ========================================================================================================
    // 主体运行
    // ========================================================================================================

    modifyPageDoc();

})();


// 功能优化
// TODO 使用倒序浏览替代large_page_num
// TODO 保存文本链接处理
// TODO 版面浮动名片、好友浮动名片添加代表作、关注、拉黑
// TODO 进度条优化:saveThread中的getAllPageContent
// TODO 自动切换全帖/选中:显示已选
// TODO 滚动条悬停显示
// TODO 设置按钮hover
// TODO 支持firefox
// TODO 保存的文件名是否要带小分区名
// TODO 考虑op未加载的情况

// 设置优化
// TODO 换行参数
// TODO 提醒参数
// TODO 获取帖子信息参数
// TODO 显示按钮和订阅更新分开设置
// TODO 代表作数量
// TODO 历史消息上限
// TODO 插入TAB设置
// TODO 恢复默认设置
// TODO 是否重命名附件

// 调试模式
// TODO 导出关注
// TODO 删除键值

// 代码优化
// TODO 添加debug log
// TODO css classname data清理
// TODO 使用?. ?? 运算符替代if判断
// TODO 使用hasOwnProperty替代in判断
// FIXME getSpaceAuthor
// TODO 使用nodename替代tagname
// TODO 避免getAllPageContent中first page重复获取

// 搁置: 麻烦
// FIXME 置顶重复
// FIXME 历史消息重复
// TODO md格式
// TODO 图片不区分楼层

// NOTE 可能会用到 @require https://scriptcat.org/lib/513/2.0.0/ElementGetter.js