NGA Filter

NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。

As of 2024-04-02. See the latest version.

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

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

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

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

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

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

(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        NGA Filter
// @namespace   https://greatest.deepsurf.us/users/263018
// @version     2.2.7
// @author      snyssss
// @description NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。
// @license     MIT

// @match       *://bbs.nga.cn/*
// @match       *://ngabbs.com/*
// @match       *://nga.178.com/*

// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand
// @grant       unsafeWindow

// @run-at      document-start
// @noframes
// ==/UserScript==

(() => {
  // 声明泥潭主模块、主题模块、回复模块
  let commonui, topicModule, replyModule;

  // KEY
  const DATA_KEY = "NGAFilter";
  const USER_AGENT_KEY = "USER_AGENT_KEY";
  const PRE_FILTER_KEY = "PRE_FILTER_KEY";
  const CLEAR_TIME_KEY = "CLEAR_TIME_KEY";

  // TIPS
  const TIPS = {
    filterMode:
      "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承",
    addTags: `一次性添加多个标记用"|"隔开,不会添加重名标记`,
    keyword: `支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。`,
    hunter: "猎巫模块需要占用额外的资源,请谨慎开启",
  };

  // STYLE
  GM_addStyle(`
    .filter-table-wrapper {
        max-height: 80vh;
        overflow-y: auto;
    }
    .filter-table {
        margin: 0;
    }
    .filter-table th,
    .filter-table td {
        position: relative;
        white-space: nowrap;
    }
    .filter-table th {
        position: sticky;
        top: 2px;
        z-index: 1;
    }
    .filter-table input:not([type]), .filter-table input[type="text"] {
        margin: 0;
        box-sizing: border-box;
        height: 100%;
        width: 100%;
    }
    .filter-input-wrapper {
        position: absolute;
        top: 6px;
        right: 6px;
        bottom: 6px;
        left: 6px;
    }
    .filter-text-ellipsis {
        display: flex;
    }
    .filter-text-ellipsis > * {
        flex: 1;
        width: 1px;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .filter-button-group {
        margin: -.1em -.2em;
    }
    .filter-tags {
        margin: 2px -0.2em 0;
        text-align: left;
    }
    .filter-mask {
        margin: 1px;
        color: #81C7D4;
        background: #81C7D4;
    }
    .filter-mask-block {
        display: block;
        border: 1px solid #66BAB7;
        text-align: center !important;
    }
    .filter-input-wrapper {
      position: absolute;
      top: 6px;
      right: 6px;
      bottom: 6px;
      left: 6px;
    }
  `);

  /**
   * 工具类
   */
  class Tools {
    /**
     * 返回当前值的类型
     * @param   {*}      value  值
     * @returns {String}        值的类型
     */
    static getType = (value) => {
      return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
    };

    /**
     * 返回当前值是否为指定的类型
     * @param   {*}               value  值
     * @param   {Array<String>}   types  类型名称集合
     * @returns {Boolean}         值是否为指定的类型
     */
    static isType = (value, ...types) => {
      return types.includes(this.getType(value));
    };

    /**
     * 拦截属性
     * @param {Object}    target    目标对象
     * @param {String}    property  属性或函数名称
     * @param {Function}  beforeGet 获取属性前事件
     * @param {Function}  beforeSet 设置属性前事件
     * @param {Function}  afterGet  获取属性后事件
     * @param {Function}  afterSet  设置属性前事件
     */
    static interceptProperty = (
      target,
      property,
      { beforeGet, beforeSet, afterGet, afterSet }
    ) => {
      // 缓存数据
      let source = target[property];

      // 如果已经有结果,则直接处理写入后操作
      if (Object.hasOwn(target, property)) {
        if (afterSet) {
          afterSet.apply(target, [source]);
        }
      }

      // 拦截
      Object.defineProperty(target, property, {
        get: () => {
          // 如果是函数
          if (this.isType(source, "function")) {
            return (...args) => {
              try {
                // 执行前操作
                // 可以在这一步修改参数
                // 可以通过在这一步抛出来阻止执行
                if (beforeGet) {
                  args = beforeGet.apply(target, args);
                }

                // 执行函数
                const returnValue = source.apply(target, args);

                // 返回的可能是一个 Promise
                const result =
                  returnValue instanceof Promise
                    ? returnValue
                    : Promise.resolve(returnValue);

                // 执行后操作
                if (afterGet) {
                  result.then((value) => {
                    afterGet.apply(target, [value, args, source]);
                  });
                }
              } catch {}
            };
          }

          try {
            // 返回前操作
            // 可以在这一步修改返回结果
            // 可以通过在这一步抛出来返回 undefined
            const result = beforeGet
              ? beforeGet.apply(target, [source])
              : source;

            // 返回后操作
            // 实际上是在返回前完成的,并不能叫返回后操作,但是我们可以配合 beforeGet 来操作处理后的数据
            if (afterGet) {
              afterGet.apply(target, [result, source]);
            }

            // 返回结果
            return result;
          } catch {
            return undefined;
          }
        },
        set: (value) => {
          try {
            // 写入前操作
            // 可以在这一步修改写入结果
            // 可以通过在这一步抛出来写入 undefined
            const result = beforeSet
              ? beforeSet.apply(target, [source, value])
              : value;

            // 写入结果
            source = result;

            // 写入后操作
            if (afterSet) {
              afterSet.apply(target, [result, value]);
            }
          } catch {
            source = undefined;
          }
        },
      });
    };

    /**
     * 合并数据
     * @param   {*}     target  目标对象
     * @param   {Array} sources 来源对象集合
     * @returns                 合并后的对象
     */
    static merge = (target, ...sources) => {
      for (const source of sources) {
        const targetType = this.getType(target);
        const sourceType = this.getType(source);

        // 如果来源对象的类型与目标对象不一致,替换为来源对象
        if (sourceType !== targetType) {
          target = source;
          continue;
        }

        // 如果来源对象是数组,直接合并
        if (targetType === "array") {
          target = [...target, ...source];
          continue;
        }

        // 如果来源对象是对象,合并对象
        if (sourceType === "object") {
          for (const key in source) {
            if (Object.hasOwn(target, key)) {
              target[key] = this.merge(target[key], source[key]);
            } else {
              target[key] = source[key];
            }
          }
          continue;
        }

        // 其他情况,更新值
        target = source;
      }

      return target;
    };

    /**
     * 数组排序
     * @param {Array}                    collection 数据集合
     * @param {Array<String | Function>} iterators  迭代器,要排序的属性名或排序函数
     */
    static sortBy = (collection, ...iterators) =>
      collection.slice().sort((a, b) => {
        for (let i = 0; i < iterators.length; i += 1) {
          const iteratee = iterators[i];

          const valueA = this.isType(iteratee, "function")
            ? iteratee(a)
            : a[iteratee];
          const valueB = this.isType(iteratee, "function")
            ? iteratee(b)
            : b[iteratee];

          if (valueA < valueB) {
            return -1;
          }

          if (valueA > valueB) {
            return 1;
          }
        }

        return 0;
      });

    /**
     * 读取论坛数据
     * @param {Response}  response  请求响应
     * @param {Boolean}   toJSON    是否转为 JSON 格式
     */
    static readForumData = async (response, toJSON = true) => {
      return new Promise(async (resolve) => {
        const blob = await response.blob();

        const reader = new FileReader();

        reader.onload = () => {
          const text = reader.result.replace(
            "window.script_muti_get_var_store=",
            ""
          );

          if (toJSON) {
            try {
              resolve(JSON.parse(text));
            } catch {
              resolve({});
            }
            return;
          }

          resolve(text);
        };

        reader.readAsText(blob, "GBK");
      });
    };

    /**
     * 获取成对括号的内容
     * @param   {String} content 内容
     * @param   {String} keyword 起始位置关键字
     * @param   {String} start   左括号
     * @param   {String} end     右括号
     * @returns {String}         包含括号的内容
     */
    static searchPair = (content, keyword, start = "{", end = "}") => {
      // 获取成对括号的位置
      const getLastIndex = (content, position, start = "{", end = "}") => {
        if (position >= 0) {
          let nextIndex = position + 1;

          while (nextIndex < content.length) {
            if (content[nextIndex] === end) {
              return nextIndex;
            }

            if (content[nextIndex] === start) {
              nextIndex = getLastIndex(content, nextIndex, start, end);

              if (nextIndex < 0) {
                break;
              }
            }

            nextIndex = nextIndex + 1;
          }
        }

        return -1;
      };

      // 起始位置
      const str = keyword + start;

      // 起始下标
      const index = content.indexOf(str) + str.length;

      // 结尾下标
      const lastIndex = getLastIndex(content, index, start, end);

      if (lastIndex >= 0) {
        return start + content.substring(index, lastIndex) + end;
      }

      return null;
    };

    /**
     * 计算字符串的颜色
     *
     * 采用的是泥潭的颜色方案,参见 commonui.htmlName
     * @param   {String} value 字符串
     * @returns {String}       RGB代码
     */
    static generateColor(value) {
      const hash = (() => {
        let h = 5381;

        for (var i = 0; i < value.length; i++) {
          h = ((h << 5) + h + value.charCodeAt(i)) & 0xffffffff;
        }

        return h;
      })();

      const hex = Math.abs(hash).toString(16) + "000000";

      const hsv = [
        `0x${hex.substring(2, 4)}` / 255,
        `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
        `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
      ];

      const rgb = commonui.hsvToRgb(hsv[0], hsv[1], hsv[2]);

      return ["#", ...rgb].reduce((a, b) => {
        return a + ("0" + b.toString(16)).slice(-2);
      });
    }
  }

  /**
   * IndexedDB
   *
   * 简单制造轮子,暂不打算引入 dexie.js,待其云方案正式推出后再考虑
   */

  class DBStorage {
    /**
     * 数据库名称
     */
    name = "NGA_FILTER_CACHE";

    /**
     * 模块列表
     */
    modules = {};

    /**
     * 当前实例
     */
    instance = null;

    /**
     * 初始化
     * @param {*} modules 模块列表
     */
    constructor(modules) {
      this.modules = modules;
    }

    /**
     * 是否支持
     */
    isSupport() {
      return unsafeWindow.indexedDB !== undefined;
    }

    /**
     * 打开数据库并创建表
     * @returns {Promise<IDBDatabase>} 实例
     */
    async open() {
      // 创建实例
      if (this.instance === null) {
        // 声明一个数组,用于等待全部表处理完毕
        const queue = [];

        // 创建实例
        await new Promise((resolve, reject) => {
          // 版本
          const version = Object.values(this.modules)
            .map(({ version }) => version)
            .reduce((a, b) => Math.max(a, b), 0);

          // 创建请求
          const request = unsafeWindow.indexedDB.open(this.name, version);

          // 创建或者升级表
          request.onupgradeneeded = (event) => {
            this.instance = event.target.result;

            const transaction = event.target.transaction;
            const oldVersion = event.oldVersion;

            Object.entries(this.modules).forEach(([key, values]) => {
              if (values.version > oldVersion) {
                queue.push(this.createOrUpdateStore(key, values, transaction));
              }
            });
          };

          // 成功后处理
          request.onsuccess = (event) => {
            this.instance = event.target.result;
            resolve();
          };

          // 失败后处理
          request.onerror = () => {
            reject();
          };
        });

        // 等待全部表处理完毕
        await Promise.all(queue);
      }

      // 返回实例
      return this.instance;
    }

    /**
     * 获取表
     * @param   {String}          name        表名
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @param   {String}          mode        事务模式,默认为只读
     * @returns {Promise<IDBObjectStore>}     表
     */
    async getStore(name, transaction = null, mode = "readonly") {
      const db = await this.open();

      if (transaction === null) {
        transaction = db.transaction(name, mode);
      }

      return transaction.objectStore(name);
    }

    /**
     * 创建或升级表
     * @param   {String}          name        表名
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async createOrUpdateStore(name, { keyPath, indexes }, transaction) {
      const db = transaction.db;
      const data = [];

      // 检查是否存在表,如果存在,缓存数据并删除旧表
      if (db.objectStoreNames.contains(name)) {
        // 获取并缓存全部数据
        const result = await this.bulkGet(name, [], transaction);

        if (result) {
          data.push(...result);
        }

        // 删除旧表
        db.deleteObjectStore(name);
      }

      // 创建表
      const store = db.createObjectStore(name, {
        keyPath,
      });

      // 创建索引
      if (indexes) {
        indexes.forEach((index) => {
          store.createIndex(index, index);
        });
      }

      // 迁移数据
      if (data.length > 0) {
        await this.bulkAdd(name, data, transaction);
      }
    }

    /**
     * 插入指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async add(name, data, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction, "readwrite");

      // 插入数据
      const result = await new Promise((resolve, reject) => {
        // 创建请求
        const request = store.add(data);

        // 成功后处理
        request.onsuccess = (event) => {
          resolve(event.target.result);
        };

        // 失败后处理
        request.onerror = (event) => {
          reject(event);
        };
      });

      // 返回结果
      return result;
    }

    /**
     * 删除指定表的数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async delete(name, key, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction, "readwrite");

      // 删除数据
      const result = await new Promise((resolve, reject) => {
        // 创建请求
        const request = store.delete(key);

        // 成功后处理
        request.onsuccess = (event) => {
          resolve(event.target.result);
        };

        // 失败后处理
        request.onerror = (event) => {
          reject(event);
        };
      });

      // 返回结果
      return result;
    }

    /**
     * 插入或修改指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async put(name, data, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction, "readwrite");

      // 插入或修改数据
      const result = await new Promise((resolve, reject) => {
        // 创建请求
        const request = store.put(data);

        // 成功后处理
        request.onsuccess = (event) => {
          resolve(event.target.result);
        };

        // 失败后处理
        request.onerror = (event) => {
          reject(event);
        };
      });

      // 返回结果
      return result;
    }

    /**
     * 获取指定表的数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}                     数据
     */
    async get(name, key, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction);

      // 查询数据
      const result = await new Promise((resolve, reject) => {
        // 创建请求
        const request = store.get(key);

        // 成功后处理
        request.onsuccess = (event) => {
          resolve(event.target.result);
        };

        // 失败后处理
        request.onerror = (event) => {
          reject(event);
        };
      });

      // 返回结果
      return result;
    }

    /**
     * 批量插入指定表的数据
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise<number>}             成功数量
     */
    async bulkAdd(name, data, transaction = null) {
      // 等待操作结果
      const result = await Promise.all(
        data.map((item) =>
          this.add(name, item, transaction)
            .then(() => true)
            .catch(() => false)
        )
      );

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量删除指定表的数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则删除全部
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise<number>}             成功数量,删除全部时返回 -1
     */
    async bulkDelete(name, keys = [], transaction = null) {
      // 如果 keys 为空,删除全部数据
      if (keys.length === 0) {
        // 获取表
        const store = await this.getStore(name, transaction, "readwrite");

        // 清空数据
        await new Promise((resolve, reject) => {
          // 创建请求
          const request = store.clear();

          // 成功后处理
          request.onsuccess = (event) => {
            resolve(event.target.result);
          };

          // 失败后处理
          request.onerror = (event) => {
            reject(event);
          };
        });

        return -1;
      }

      // 等待操作结果
      const result = await Promise.all(
        data.map((item) =>
          this.delete(name, item, transaction)
            .then(() => true)
            .catch(() => false)
        )
      );

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量插入或修改指定表的数据
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise<number>}             成功数量
     */
    async bulkPut(name, data, transaction = null) {
      // 等待操作结果
      const result = await Promise.all(
        data.map((item) =>
          this.put(name, item, transaction)
            .then(() => true)
            .catch(() => false)
        )
      );

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量获取指定表的数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则获取全部
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise<Array>}              数据集合
     */
    async bulkGet(name, keys = [], transaction = null) {
      // 如果 keys 为空,查询全部数据
      if (keys.length === 0) {
        // 获取表
        const store = await this.getStore(name, transaction);

        // 查询数据
        const result = await new Promise((resolve, reject) => {
          // 创建请求
          const request = store.getAll();

          // 成功后处理
          request.onsuccess = (event) => {
            resolve(event.target.result || []);
          };

          // 失败后处理
          request.onerror = (event) => {
            reject(event);
          };
        });

        // 返回结果
        return result;
      }

      // 返回符合的结果
      const result = [];

      await Promise.all(
        keys.map((key) =>
          this.get(name, key, transaction)
            .then((item) => {
              result.push(item);
            })
            .catch(() => {})
        )
      );

      return result;
    }
  }

  /**
   * 油猴存储
   *
   * 虽然使用了不支持 Promise 的 GM_getValue 与 GM_setValue,但是为了配合 IndexedDB,统一视为 Promise
   */
  class GMStorage extends DBStorage {
    /**
     * 初始化
     * @param {*} modules 模块列表
     */
    constructor(modules) {
      super(modules);
    }

    /**
     * 插入指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async add(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, data);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.add(name, data);
      }

      // 获取对应的主键
      const keyPath = this.modules[name].keyPath;
      const key = data[keyPath];

      // 如果数据中不包含主键,抛出异常
      if (key === undefined) {
        throw new Error();
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 如果对应主键已存在,抛出异常
      if (Object.hasOwn(values, key)) {
        throw new Error();
      }

      // 插入数据
      values[key] = data;

      // 保存数据
      GM_setValue(name, values);
    }

    /**
     * 删除指定表的数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @returns {Promise}
     */
    async delete(name, key) {
      // 如果不在模块列表里,忽略 key,删除全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, {});
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.delete(name, key);
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 如果对应主键不存在,抛出异常
      if (Object.hasOwn(values, key) === false) {
        throw new Error();
      }

      // 删除数据
      delete values[key];

      // 保存数据
      GM_setValue(name, values);
    }

    /**
     * 插入或修改指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async put(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, data);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.put(name, data);
      }

      // 获取对应的主键
      const keyPath = this.modules[name].keyPath;
      const key = data[keyPath];

      // 如果数据中不包含主键,抛出异常
      if (key === undefined) {
        throw new Error();
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 插入或修改数据
      values[key] = data;

      // 保存数据
      GM_setValue(name, values);
    }

    /**
     * 获取指定表的数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @returns {Promise}                     数据
     */
    async get(name, key) {
      // 如果不在模块列表里,忽略 key,返回全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_getValue(name);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.get(name, key);
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 如果对应主键不存在,抛出异常
      if (Object.hasOwn(values, key) === false) {
        throw new Error();
      }

      // 返回结果
      return values[key];
    }

    /**
     * 批量插入指定表的数据
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @returns {Promise<number>}             成功数量
     */
    async bulkAdd(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, {});
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.bulkAdd(name, data);
      }

      // 获取对应的主键
      const keyPath = this.modules[name].keyPath;

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 添加数据
      const result = data.map((item) => {
        const key = item[keyPath];

        // 如果数据中不包含主键,抛出异常
        if (key === undefined) {
          return false;
        }

        // 如果对应主键已存在,抛出异常
        if (Object.hasOwn(values, key)) {
          return false;
        }

        // 插入数据
        values[key] = item;

        return true;
      });

      // 保存数据
      GM_setValue(name, values);

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量删除指定表的数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则删除全部
     * @returns {Promise<number>}             成功数量,删除全部时返回 -1
     */
    async bulkDelete(name, keys = []) {
      // 如果不在模块列表里,忽略 keys,删除全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, {});
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.bulkDelete(name, keys);
      }

      // 如果 keys 为空,删除全部数据
      if (keys.length === 0) {
        GM_setValue(name, {});

        return -1;
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 删除数据
      const result = keys.map((key) => {
        // 如果对应主键不存在,抛出异常
        if (Object.hasOwn(values, key) === false) {
          return false;
        }

        // 删除数据
        delete values[key];

        return true;
      });

      // 保存数据
      GM_setValue(name, values);

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量插入或修改指定表的数据
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @returns {Promise<number>}             成功数量
     */
    async bulkPut(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, data);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.bulkPut(name, keys);
      }

      // 获取对应的主键
      const keyPath = this.modules[name].keyPath;

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 添加数据
      const result = data.map((item) => {
        const key = item[keyPath];

        // 如果数据中不包含主键,抛出异常
        if (key === undefined) {
          return false;
        }

        // 插入数据
        values[key] = item;

        return true;
      });

      // 保存数据
      GM_setValue(name, values);

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量获取指定表的数据,如果不在模块列表里,返回全部数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则获取全部
     * @returns {Promise<Array>}              数据集合
     */
    async bulkGet(name, keys = []) {
      // 如果不在模块列表里,忽略 keys,返回全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_getValue(name);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.bulkGet(name, keys);
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 如果 keys 为空,返回全部数据
      if (keys.length === 0) {
        return Object.values(values);
      }

      // 返回符合的结果
      const result = [];

      keys.forEach((key) => {
        if (Object.hasOwn(values, key)) {
          result.push(values[key]);
        }
      });

      return result;
    }
  }

  /**
   * 缓存管理
   *
   * 在存储的基础上,增加了过期时间和持久化选项,自动清理缓存
   */
  class Cache extends GMStorage {
    /**
     * 增加模块列表的 timestamp 索引
     * @param {*} modules 模块列表
     */
    constructor(modules) {
      Object.values(modules).forEach((item) => {
        item.indexes = item.indexes || [];

        if (item.indexes.includes("timestamp") === false) {
          item.indexes.push("timestamp");
        }
      });

      super(modules);

      this.autoClear();
    }

    /**
     * 插入指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async add(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(this.modules, name)) {
        data.timestamp = data.timestamp || new Date().getTime();
      }

      return super.add(name, data);
    }

    /**
     * 插入或修改指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async put(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(this.modules, name)) {
        data.timestamp = data.timestamp || new Date().getTime();
      }

      return super.put(name, data);
    }

    /**
     * 获取指定表的数据,并移除过期数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @returns {Promise}                     数据
     */
    async get(name, key) {
      // 获取数据
      const value = await super.get(name, key).catch(() => null);

      // 如果不在模块里,直接返回结果
      if (Object.hasOwn(this.modules, name) === false) {
        return value;
      }

      // 如果有结果的话,移除超时数据
      if (value) {
        // 读取模块配置
        const { expireTime, persistent } = this.modules[name];

        // 持久化或未超时
        if (persistent || value.timestamp + expireTime > new Date().getTime()) {
          return value;
        }

        // 移除超时数据
        await super.delete(name, key);
      }

      return null;
    }

    /**
     * 批量插入指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @returns {Promise<number>}             成功数量
     */
    async bulkAdd(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(this.modules, name)) {
        data.forEach((item) => {
          item.timestamp = item.timestamp || new Date().getTime();
        });
      }

      return super.bulkAdd(name, data);
    }

    /**
     * 批量删除指定表的数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则删除全部
     * @param   {boolean}         force       是否强制删除,否则只删除过期数据
     * @returns {Promise<number>}             成功数量,删除全部时返回 -1
     */
    async bulkDelete(name, keys = [], force = false) {
      // 如果不在模块里,强制删除
      if (Object.hasOwn(this.modules, name) === false) {
        force = true;
      }

      // 强制删除
      if (force) {
        return super.bulkDelete(name, keys);
      }

      // 批量获取指定表的数据,并移除过期数据
      const result = this.bulkGet(name, keys);

      // 返回成功数量
      if (keys.length === 0) {
        return -1;
      }

      return keys.length - result.length;
    }

    /**
     * 批量插入或修改指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @returns {Promise<number>}             成功数量
     */
    async bulkPut(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(this.modules, name)) {
        data.forEach((item) => {
          item.timestamp = item.timestamp || new Date().getTime();
        });
      }

      return super.bulkPut(name, data);
    }

    /**
     * 批量获取指定表的数据,并移除过期数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则获取全部
     * @returns {Promise<Array>}              数据集合
     */
    async bulkGet(name, keys = []) {
      // 获取数据
      const values = await super.bulkGet(name, keys).catch(() => []);

      // 如果不在模块里,直接返回结果
      if (Object.hasOwn(this.modules, name) === false) {
        return values;
      }

      // 读取模块配置
      const { keyPath, expireTime, persistent } = this.modules[name];

      // 筛选出超时数据
      const result = [];
      const expired = [];

      values.forEach((value) => {
        // 持久化或未超时
        if (persistent || value.timestamp + expireTime > new Date().getTime()) {
          result.push(value);
          return;
        }

        // 记录超时数据
        expired.push(value[keyPath]);
      });

      // 移除超时数据
      await super.bulkDelete(name, expired);

      // 返回结果
      return result;
    }

    /**
     * 自动清理缓存
     */
    async autoClear() {
      const data = await this.get(CLEAR_TIME_KEY);

      const now = new Date();
      const clearTime = new Date(data || 0);

      const isToday =
        now.getDate() === clearTime.getDate() &&
        now.getMonth() === clearTime.getMonth() &&
        now.getFullYear() === clearTime.getFullYear();

      if (isToday) {
        return;
      }

      await Promise.all(
        Object.keys(this.modules).map((name) => this.bulkDelete(name))
      );

      await this.put(CLEAR_TIME_KEY, now.getTime());
    }
  }

  /**
   * 设置
   *
   * 暂时整体处理模块设置,后续再拆分
   */
  class Settings {
    /**
     * 缓存管理
     */
    cache;

    /**
     * 当前设置
     */
    data = null;

    /**
     * 初始化并绑定缓存管理
     * @param {Cache} cache 缓存管理
     */
    constructor(cache) {
      this.cache = cache;
    }

    /**
     * 读取设置
     */
    async load() {
      // 读取设置
      if (this.data === null) {
        // 默认配置
        const defaultData = {
          tags: {},
          users: {},
          keywords: {},
          locations: {},
          options: {
            filterRegdateLimit: 0,
            filterPostnumLimit: 0,
            filterTopicRateLimit: 100,
            filterReputationLimit: NaN,
            filterAnony: false,
            filterMode: "隐藏",
          },
        };

        // 读取数据
        const storedData = await this.cache
          .get(DATA_KEY)
          .then((values) => values || {});

        // 写入缓存
        this.data = Tools.merge({}, defaultData, storedData);

        // 写入默认模块选项
        if (Object.hasOwn(this.data, "modules") === false) {
          this.data.modules = ["user", "tag", "misc"];

          if (Object.keys(this.data.keywords).length > 0) {
            this.data.modules.push("keyword");
          }

          if (Object.keys(this.data.locations).length > 0) {
            this.data.modules.push("location");
          }
        }
      }

      // 返回设置
      return this.data;
    }

    /**
     * 写入设置
     */
    async save() {
      return this.cache.put(DATA_KEY, this.data);
    }

    /**
     * 获取模块列表
     */
    get modules() {
      return this.data.modules;
    }

    /**
     * 设置模块列表
     */
    set modules(values) {
      this.data.modules = values;
      this.save();
    }

    /**
     * 获取标签列表
     */
    get tags() {
      return this.data.tags;
    }

    /**
     * 设置标签列表
     */
    set tags(values) {
      this.data.tags = values;
      this.save();
    }

    /**
     * 获取用户列表
     */
    get users() {
      return this.data.users;
    }

    /**
     * 设置用户列表
     */
    set users(values) {
      this.data.users = values;
      this.save();
    }

    /**
     * 获取关键字列表
     */
    get keywords() {
      return this.data.keywords;
    }

    /**
     * 设置关键字列表
     */
    set keywords(values) {
      this.data.keywords = values;
      this.save();
    }

    /**
     * 获取属地列表
     */
    get locations() {
      return this.data.locations;
    }

    /**
     * 设置属地列表
     */
    set locations(values) {
      this.data.locations = values;
      this.save();
    }

    /**
     * 获取默认过滤模式
     */
    get defaultFilterMode() {
      return this.data.options.filterMode;
    }

    /**
     * 设置默认过滤模式
     */
    set defaultFilterMode(value) {
      this.data.options.filterMode = value;
      this.save();
    }

    /**
     * 获取注册时间限制
     */
    get filterRegdateLimit() {
      return this.data.options.filterRegdateLimit || 0;
    }

    /**
     * 设置注册时间限制
     */
    set filterRegdateLimit(value) {
      this.data.options.filterRegdateLimit = value;
      this.save();
    }

    /**
     * 获取发帖数量限制
     */
    get filterPostnumLimit() {
      return this.data.options.filterPostnumLimit || 0;
    }

    /**
     * 设置发帖数量限制
     */
    set filterPostnumLimit(value) {
      this.data.options.filterPostnumLimit = value;
      this.save();
    }

    /**
     * 获取发帖比例限制
     */
    get filterTopicRateLimit() {
      return this.data.options.filterTopicRateLimit || 100;
    }

    /**
     * 设置发帖比例限制
     */
    set filterTopicRateLimit(value) {
      this.data.options.filterTopicRateLimit = value;
      this.save();
    }

    /**
     * 获取版面声望限制
     */
    get filterReputationLimit() {
      return this.data.options.filterReputationLimit || NaN;
    }

    /**
     * 设置版面声望限制
     */
    set filterReputationLimit(value) {
      this.data.options.filterReputationLimit = value;
      this.save();
    }

    /**
     * 获取是否过滤匿名
     */
    get filterAnonymous() {
      return this.data.options.filterAnony || false;
    }

    /**
     * 设置是否过滤匿名
     */
    set filterAnonymous(value) {
      this.data.options.filterAnony = value;
      this.save();
    }

    /**
     * 获取代理设置
     */
    get userAgent() {
      return this.cache.get(USER_AGENT_KEY).then((value) => {
        if (value === undefined) {
          return "Nga_Official";
        }

        return value;
      });
    }

    /**
     * 修改代理设置
     */
    set userAgent(value) {
      this.cache.put(USER_AGENT_KEY, value).then(() => {
        location.reload();
      });
    }

    /**
     * 获取是否启用前置过滤
     */
    get preFilterEnabled() {
      return this.cache.get(PRE_FILTER_KEY).then((value) => {
        if (value === undefined) {
          return true;
        }

        return value;
      });
    }

    /**
     * 设置是否启用前置过滤
     */
    set preFilterEnabled(value) {
      this.cache.put(PRE_FILTER_KEY, value).then(() => {
        location.reload();
      });
    }

    /**
     * 获取过滤模式列表
     *
     * 模拟成从配置中获取
     */
    get filterModes() {
      return ["继承", "标记", "遮罩", "隐藏", "显示"];
    }

    /**
     * 获取指定下标过滤模式
     * @param {Number} index 下标
     */
    getNameByMode(index) {
      const modes = this.filterModes;

      return modes[index] || "";
    }

    /**
     * 获取指定过滤模式下标
     * @param {String} name 过滤模式
     */
    getModeByName(name) {
      const modes = this.filterModes;

      return modes.indexOf(name);
    }

    /**
     * 切换过滤模式
     * @param   {String} value 过滤模式
     * @returns {String}       过滤模式
     */
    switchModeByName(value) {
      const index = this.getModeByName(value);

      const nextIndex = (index + 1) % this.filterModes.length;

      return this.filterModes[nextIndex];
    }
  }

  /**
   * API
   */
  class API {
    /**
     * 缓存模块
     */
    static modules = {
      TOPIC_NUM_CACHE: {
        keyPath: "uid",
        version: 1,
        expireTime: 1000 * 60 * 60,
        persistent: true,
      },
      USER_INFO_CACHE: {
        keyPath: "uid",
        version: 1,
        expireTime: 1000 * 60 * 60,
        persistent: false,
      },
      PAGE_CACHE: {
        keyPath: "url",
        version: 1,
        expireTime: 1000 * 60 * 10,
        persistent: false,
      },
      FORUM_POSTED_CACHE: {
        keyPath: "url",
        version: 2,
        expireTime: 1000 * 60 * 60 * 24,
        persistent: true,
      },
    };

    /**
     * 缓存管理
     */
    cache;

    /**
     * 设置
     */
    settings;

    /**
     * 初始化并绑定缓存管理、设置
     * @param {Cache}     cache     缓存管理
     * @param {Settings}  settings  设置
     */
    constructor(cache, settings) {
      this.cache = cache;
      this.settings = settings;
    }

    /**
     * 简单的统一请求
     * @param {String}  url    请求地址
     * @param {Object}  config 请求参数
     * @param {Boolean} toJSON 是否转为 JSON 格式
     */
    async request(url, config = {}, toJSON = true) {
      const userAgent = await this.settings.userAgent;

      const response = await fetch(url, {
        headers: {
          "X-User-Agent": userAgent,
        },
        ...config,
      });

      const result = await Tools.readForumData(response, toJSON);

      return result;
    }

    /**
     * 获取用户主题数量
     * @param {number} uid 用户 ID
     */
    async getTopicNum(uid) {
      const name = "TOPIC_NUM_CACHE";
      const expireTime = API.modules[name];

      const api = `/thread.php?lite=js&authorid=${uid}`;

      const cache = await this.cache.get(name, uid);

      // 仍在缓存期间内,直接返回
      if (cache) {
        const expired = cache.timestamp + expireTime < new Date().getTime();

        if (expired === false) {
          return cache.count;
        }
      }

      // 请求数据
      const result = await this.request(api);

      // 服务器可能返回错误,遇到这种情况下,需要保留缓存
      const count = (() => {
        if (result.data) {
          return result.data.__ROWS || 0;
        }

        if (cache) {
          return cache.count;
        }

        return 0;
      })();

      // 更新缓存
      this.cache.put(name, {
        uid,
        count,
      });

      return count;
    }

    /**
     * 获取用户信息
     * @param {number} uid 用户 ID
     */
    async getUserInfo(uid) {
      const name = "USER_INFO_CACHE";

      const api = `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`;

      const cache = await this.cache.get(name, uid);

      if (cache) {
        return cache.data;
      }

      const result = await this.request(api, {
        credentials: "omit",
      });

      const data = result.data ? result.data[0] : null;

      if (data) {
        this.cache.put(name, {
          uid,
          data,
        });
      }

      return data || {};
    }

    /**
     * 获取帖子内容、用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、版面声望
     * @param {number} tid 主题 ID
     * @param {number} pid 回复 ID
     */
    async getPostInfo(tid, pid) {
      const name = "PAGE_CACHE";

      const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;

      const cache = await this.cache.get(name, api);

      if (cache) {
        return cache.data;
      }

      const result = await this.request(api, {}, false);

      const parser = new DOMParser();

      const doc = parser.parseFromString(result, "text/html");

      // 验证帖子正常
      const verify = doc.querySelector("#m_posts");

      if (verify === null) {
        return {};
      }

      // 声明返回值
      const data = {};

      // 取得顶楼 UID
      data.uid = (() => {
        const ele = doc.querySelector("#postauthor0");

        if (ele) {
          const res = ele.getAttribute("href").match(/uid=(\S+)/);

          if (res) {
            return res[1];
          }
        }

        return 0;
      })();

      // 取得顶楼标题
      data.subject = doc.querySelector("#postsubject0").innerHTML;

      // 取得顶楼内容
      data.content = doc.querySelector("#postcontent0").innerHTML;

      // 非匿名用户可以继续取得用户信息和版面声望
      if (data.uid > 0) {
        // 取得用户信息
        data.userInfo = (() => {
          const text = Tools.searchPair(result, `"${data.uid}":`);

          if (text) {
            try {
              return JSON.parse(text);
            } catch {
              return null;
            }
          }

          return null;
        })();

        // 取得用户声望
        data.reputation = (() => {
          const reputations = (() => {
            const text = Tools.searchPair(result, `"__REPUTATIONS":`);

            if (text) {
              try {
                return JSON.parse(text);
              } catch {
                return null;
              }
            }

            return null;
          })();

          if (reputations) {
            for (let fid in reputations) {
              return reputations[fid][data.uid] || 0;
            }
          }

          return NaN;
        })();
      }

      // 写入缓存
      this.cache.put(name, {
        url: api,
        data,
      });

      // 返回结果
      return data;
    }

    /**
     * 获取版面信息
     * @param {number} fid 版面 ID
     */
    async getForumInfo(fid) {
      if (Number.isNaN(fid)) {
        return null;
      }

      const api = `/thread.php?lite=js&fid=${fid}`;

      const result = await this.request(api);

      const info = result.data ? result.data.__F : null;

      return info;
    }

    /**
     * 获取版面发言记录
     * @param {number} fid 版面 ID
     * @param {number} uid 用户 ID
     */
    async getForumPosted(fid, uid) {
      const name = "FORUM_POSTED_CACHE";
      const expireTime = API.modules[name];

      const api = `/thread.php?lite=js&authorid=${uid}&fid=${fid}`;

      const cache = await this.cache.get(name, api);

      if (cache) {
        // 发言是无法撤销的,只要有记录就永远不需要再获取
        // 手动处理没有记录的缓存数据
        const expired = cache.timestamp + expireTime < new Date().getTime();
        if (expired && cache.data === false) {
          await this.cache.delete(name, api);
        }

        return cache.data;
      }

      let isComplete = false;
      let isBusy = false;

      const func = async (url) => {
        if (isComplete || isBusy) {
          return;
        }

        const result = await this.request(url, {}, false);

        // 将所有匹配的 FID 写入缓存,即使并不在设置里
        const matched = result.match(/"fid":(-?\d+),/g);

        if (matched) {
          const list = [
            ...new Set(
              matched.map((item) => parseInt(item.match(/-?\d+/)[0], 10))
            ),
          ];

          list.forEach((item) => {
            const key = api.replace(`&fid=${fid}`, `&fid=${item}`);

            // 写入缓存
            this.cache.put(name, {
              url: key,
              data: true,
            });

            // 已有结果,无需继续查询
            if (fid === item) {
              isComplete = true;
            }
          });
        }

        // 泥潭给版面查询接口增加了限制,经常会出现“服务器忙,请稍后重试”的错误
        if (result.indexOf("服务器忙") > 0) {
          isBusy = true;
        }
      };

      // 先获取回复记录的第一页,顺便可以获取其他版面的记录
      // 没有再通过版面接口获取,避免频繁出现“服务器忙,请稍后重试”的错误
      await func(api.replace(`&fid=${fid}`, `&searchpost=1`));
      await func(api + "&searchpost=1");
      await func(api);

      // 无论成功与否都写入缓存
      if (isComplete === false) {
        // 遇到服务器忙的情况,手动调整缓存时间至 1 小时
        const timestamp = isBusy
          ? new Date().getTime() - (expireTime - 1000 * 60 * 60)
          : new Date().getTime();

        // 写入失败缓存
        this.cache.put(name, {
          url: api,
          data: false,
          timestamp,
        });
      }

      return isComplete;
    }
  }

  /**
   * UI
   */
  class UI {
    /**
     * 标签
     */
    static label = "屏蔽";

    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * 模块列表
     */
    modules = {};

    /**
     * 菜单元素
     */
    menu = null;

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化并绑定设置、API,注册脚本菜单
     * @param {Settings} settings 设置
     * @param {API}      api      API
     */
    constructor(settings, api) {
      this.settings = settings;
      this.api = api;

      this.init();
    }

    /**
     * 初始化,创建基础视图,初始化通用设置
     */
    init() {
      const tabs = this.createTabs({
        className: "right_",
      });

      const content = this.createElement("DIV", [], {
        style: "width: 80vw;",
      });

      const container = this.createElement("DIV", [tabs, content]);

      this.views = {
        tabs,
        content,
        container,
      };

      this.initSettings();
    }

    /**
     * 初始化设置
     */
    initSettings() {
      // 创建基础视图
      const settings = this.createElement("DIV", []);

      // 添加设置项
      const add = (order, ...elements) => {
        const items = [...settings.childNodes];

        if (items.find((item) => item.order === order)) {
          return;
        }

        const item = this.createElement(
          "DIV",
          [...elements, this.createElement("BR", [])],
          {
            order,
          }
        );

        const anchor = items.find((item) => item.order > order);

        settings.insertBefore(item, anchor || null);

        return item;
      };

      // 绑定事件
      Object.assign(settings, {
        add,
      });

      // 合并视图
      Object.assign(this.views, {
        settings,
      });

      // 创建标签页
      const { tabs, content } = this.views;

      this.createTab(tabs, "设置", Number.MAX_SAFE_INTEGER, {
        onclick: () => {
          content.innerHTML = "";
          content.appendChild(settings);
        },
      });
    }

    /**
     * 弹窗确认
     * @param   {String}  message 提示信息
     * @returns {Promise}
     */
    confirm(message = "是否确认?") {
      return new Promise((resolve, reject) => {
        const result = confirm(message);

        if (result) {
          resolve();
          return;
        }

        reject();
      });
    }

    /**
     * 折叠
     * @param {String | Number} key     标识
     * @param {HTMLElement}     element 目标元素
     * @param {String}          content 内容
     */
    collapse(key, element, content) {
      key = "collapsed_" + key;

      element.innerHTML = `
        <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7;">
            <span class="crimson">Troll must die.</span>
            <a href="javascript:void(0)" onclick="[...document.getElementsByName('${key}')].forEach(item => item.style.display = '')">点击查看</a>
            <div style="display: none;" name="${key}">
                ${content}
            </div>
        </div>`;
    }

    /**
     * 创建元素
     * @param   {String}                               tagName    标签
     * @param   {HTMLElement | HTMLElement[] | String} content    内容,元素或者 innerHTML
     * @param   {*}                                    properties 额外属性
     * @returns {HTMLElement}                                     元素
     */
    createElement(tagName, content, properties = {}) {
      const element = document.createElement(tagName);

      // 写入内容
      if (typeof content === "string") {
        element.innerHTML = content;
      } else {
        if (Array.isArray(content) === false) {
          content = [content];
        }

        content.forEach((item) => {
          if (item === null) {
            return;
          }

          if (typeof item === "string") {
            element.append(item);
            return;
          }

          element.appendChild(item);
        });
      }

      // 对 A 标签的额外处理
      if (tagName.toUpperCase() === "A") {
        if (Object.hasOwn(properties, "href") === false) {
          properties.href = "javascript: void(0);";
        }
      }

      // 附加属性
      Object.entries(properties).forEach(([key, value]) => {
        element[key] = value;
      });

      return element;
    }

    /**
     * 创建按钮
     * @param {String}   text       文字
     * @param {Function} onclick    点击事件
     * @param {*}        properties 额外属性
     */
    createButton(text, onclick, properties = {}) {
      return this.createElement("BUTTON", text, {
        ...properties,
        onclick,
      });
    }

    /**
     * 创建按钮组
     * @param {Array} buttons 按钮集合
     */
    createButtonGroup(...buttons) {
      return this.createElement("DIV", buttons, {
        className: "filter-button-group",
      });
    }

    /**
     * 创建表格
     * @param   {Array}       headers    表头集合
     * @param   {*}           properties 额外属性
     * @returns {HTMLElement}            元素和相关函数
     */
    createTable(headers, properties = {}) {
      const rows = [];

      const ths = headers.map((item, index) =>
        this.createElement("TH", item.label, {
          ...item,
          className: `c${index + 1}`,
        })
      );

      const tr =
        ths.length > 0
          ? this.createElement("TR", ths, {
              className: "block_txt_c0",
            })
          : null;

      const thead = tr !== null ? this.createElement("THEAD", tr) : null;

      const tbody = this.createElement("TBODY", []);

      const table = this.createElement("TABLE", [thead, tbody], {
        ...properties,
        className: "filter-table forumbox",
      });

      const wrapper = this.createElement("DIV", table, {
        className: "filter-table-wrapper",
      });

      const intersectionObserver = new IntersectionObserver((entries) => {
        if (entries[0].intersectionRatio <= 0) return;

        const list = rows.splice(0, 10);

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

        intersectionObserver.disconnect();

        tbody.append(...list);

        intersectionObserver.observe(tbody.lastElementChild);
      });

      const add = (...columns) => {
        const tds = columns.map((column, index) => {
          if (ths[index]) {
            const { center, ellipsis } = ths[index];

            const properties = {};

            if (center) {
              properties.style = "text-align: center;";
            }

            if (ellipsis) {
              properties.className = "filter-text-ellipsis";
            }

            column = this.createElement("DIV", column, properties);
          }

          return this.createElement("TD", column, {
            className: `c${index + 1}`,
          });
        });

        const tr = this.createElement("TR", tds, {
          className: `row${(rows.length % 2) + 1}`,
        });

        intersectionObserver.disconnect();

        rows.push(tr);

        intersectionObserver.observe(tbody.lastElementChild || tbody);
      };

      const update = (e, ...columns) => {
        const row = e.target.closest("TR");

        if (row) {
          const tds = row.querySelectorAll("TD");

          columns.map((column, index) => {
            if (ths[index]) {
              const { center, ellipsis } = ths[index];

              const properties = {};

              if (center) {
                properties.style = "text-align: center;";
              }

              if (ellipsis) {
                properties.className = "filter-text-ellipsis";
              }

              column = this.createElement("DIV", column, properties);
            }

            if (tds[index]) {
              tds[index].innerHTML = "";
              tds[index].append(column);
            }
          });
        }
      };

      const remove = (e) => {
        const row = e.target.closest("TR");

        if (row) {
          tbody.removeChild(row);
        }
      };

      const clear = () => {
        rows.splice(0);
        intersectionObserver.disconnect();

        tbody.innerHTML = "";
      };

      Object.assign(wrapper, {
        add,
        update,
        remove,
        clear,
      });

      return wrapper;
    }

    /**
     * 创建标签组
     * @param {*} properties 额外属性
     */
    createTabs(properties = {}) {
      const tabs = this.createElement(
        "DIV",
        `<table class="stdbtn" cellspacing="0">
          <tbody>
            <tr></tr>
          </tbody>
        </table>`,
        properties
      );

      return this.createElement(
        "DIV",
        [
          tabs,
          this.createElement("DIV", [], {
            className: "clear",
          }),
        ],
        {
          style: "display: none; margin-bottom: 5px;",
        }
      );
    }

    /**
     * 创建标签
     * @param {Element} tabs       标签组
     * @param {String}  label      标签名称
     * @param {Number}  order      标签顺序,重复则跳过
     * @param {*}       properties 额外属性
     */
    createTab(tabs, label, order, properties = {}) {
      const group = tabs.querySelector("TR");

      const items = [...group.childNodes];

      if (items.find((item) => item.order === order)) {
        return;
      }

      if (items.length > 0) {
        tabs.style.removeProperty("display");
      }

      const tab = this.createElement("A", label, {
        ...properties,
        className: "nobr silver",
        onclick: () => {
          if (tab.className === "nobr") {
            return;
          }

          group.querySelectorAll("A").forEach((item) => {
            if (item === tab) {
              item.className = "nobr";
            } else {
              item.className = "nobr silver";
            }
          });

          if (properties.onclick) {
            properties.onclick();
          }
        },
      });

      const wrapper = this.createElement("TD", tab, {
        order,
      });

      const anchor = items.find((item) => item.order > order);

      group.insertBefore(wrapper, anchor || null);

      return wrapper;
    }

    /**
     * 创建对话框
     * @param {HTMLElement | null} anchor  要绑定的元素,如果为空,直接弹出
     * @param {String}             title   对话框的标题
     * @param {HTMLElement}        content 对话框的内容
     */
    createDialog(anchor, title, content) {
      let window;

      const show = () => {
        if (window === undefined) {
          window = commonui.createCommmonWindow();
        }

        window._.addContent(null);
        window._.addTitle(title);
        window._.addContent(content);
        window._.show();
      };

      if (anchor) {
        anchor.onclick = show;
      } else {
        show();
      }

      return window;
    }

    /**
     * 渲染菜单
     */
    renderMenu() {
      // 如果泥潭的右上角菜单还没有加载完成,说明模块尚未加载完毕,跳过
      const anchor = document.querySelector("#mainmenu .td:last-child");

      if (anchor === null) {
        return;
      }

      const menu = this.createElement("A", this.constructor.label, {
        className: "mmdefault nobr",
      });

      const container = this.createElement("DIV", menu, {
        className: "td",
      });

      // 插入菜单
      anchor.before(container);

      // 绑定菜单元素
      this.menu = menu;
    }

    /**
     * 渲染视图
     */
    renderView() {
      // 如果菜单还没有渲染,说明模块尚未加载完毕,跳过
      if (this.menu === null) {
        return;
      }

      // 绑定菜单点击事件.
      this.createDialog(
        this.menu,
        this.constructor.label,
        this.views.container
      );

      // 启用第一个模块
      this.views.tabs.querySelector("A").click();
    }

    /**
     * 渲染
     */
    render() {
      this.renderMenu();
      this.renderView();
    }
  }

  /**
   * 基础模块
   */
  class Module {
    /**
     * 模块名称
     */
    static name;

    /**
     * 模块标签
     */
    static label;

    /**
     * 顺序
     */
    static order;

    /**
     * 依赖模块
     */
    static depends = [];

    /**
     * 附加模块
     */
    static addons = [];

    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * UI
     */
    ui;

    /**
     * 过滤列表
     */
    data = [];

    /**
     * 依赖模块
     */
    depends = {};

    /**
     * 附加模块
     */
    addons = {};

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化并绑定设置、API、UI、过滤列表,注册 UI
     * @param {Settings} settings 设置
     * @param {API}      api      API
     * @param {UI}       ui       UI
     */
    constructor(settings, api, ui, data) {
      this.settings = settings;
      this.api = api;
      this.ui = ui;

      this.data = data;

      this.init();
    }

    /**
     * 创建实例
     * @param   {Settings}      settings 设置
     * @param   {API}           api      API
     * @param   {UI}            ui       UI
     * @param   {Array}         data     过滤列表
     * @returns {Module | null}          成功后返回模块实例
     */
    static create(settings, api, ui, data) {
      // 读取设置里的模块列表
      const modules = settings.modules;

      // 如果不包含自己或依赖的模块,则返回空
      const index = [this, ...this.depends].findIndex(
        (module) => modules.includes(module.name) === false
      );

      if (index >= 0) {
        return null;
      }

      // 创建实例
      const instance = new this(settings, api, ui, data);

      // 返回实例
      return instance;
    }

    /**
     * 判断指定附加模块是否启用
     * @param {typeof Module} module 模块
     */
    hasAddon(module) {
      return Object.hasOwn(this.addons, module.name);
    }

    /**
     * 初始化,创建基础视图和组件
     */
    init() {
      if (this.views.container) {
        this.destroy();
      }

      const { ui } = this;

      const container = ui.createElement("DIV", []);

      this.views = {
        container,
      };

      this.initComponents();
    }

    /**
     * 初始化组件
     */
    initComponents() {}

    /**
     * 销毁
     */
    destroy() {
      Object.values(this.views).forEach((view) => {
        if (view.parentNode) {
          view.parentNode.removeChild(view);
        }
      });

      this.views = {};
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      container.innerHTML = "";
      container.appendChild(this.views.container);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {}

    /**
     * 通知
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async notify(item, result) {}
  }

  /**
   * 过滤器
   */
  class Filter {
    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * UI
     */
    ui;

    /**
     * 过滤列表
     */
    data = [];

    /**
     * 模块列表
     */
    modules = {};

    /**
     * 初始化并绑定设置、API、UI
     * @param {Settings} settings 设置
     * @param {API}      api      API
     * @param {UI}       ui       UI
     */
    constructor(settings, api, ui) {
      this.settings = settings;
      this.api = api;
      this.ui = ui;
    }

    /**
     * 绑定两个模块的互相关系
     * @param {Module} moduleA 模块A
     * @param {Module} moduleB 模块B
     */
    bindModule(moduleA, moduleB) {
      const nameA = moduleA.constructor.name;
      const nameB = moduleB.constructor.name;

      // A 依赖 B
      if (moduleA.constructor.depends.findIndex((i) => i.name === nameB) >= 0) {
        moduleA.depends[nameB] = moduleB;
        moduleA.init();
      }

      // B 依赖 A
      if (moduleB.constructor.depends.findIndex((i) => i.name === nameA) >= 0) {
        moduleB.depends[nameA] = moduleA;
        moduleB.init();
      }

      // A 附加 B
      if (moduleA.constructor.addons.findIndex((i) => i.name === nameB) >= 0) {
        moduleA.addons[nameB] = moduleB;
        moduleA.init();
      }

      // B 附加 A
      if (moduleB.constructor.addons.findIndex((i) => i.name === nameA) >= 0) {
        moduleB.addons[nameA] = moduleA;
        moduleB.init();
      }
    }

    /**
     * 加载模块
     * @param {typeof Module} module 模块
     */
    initModule(module) {
      // 如果已经加载过则跳过
      if (Object.hasOwn(this.modules, module.name)) {
        return;
      }

      // 创建模块
      const instance = module.create(
        this.settings,
        this.api,
        this.ui,
        this.data
      );

      // 如果创建失败则跳过
      if (instance === null) {
        return;
      }

      // 绑定依赖模块和附加模块
      Object.values(this.modules).forEach((item) => {
        this.bindModule(item, instance);
      });

      // 合并模块
      this.modules[module.name] = instance;

      // 按照顺序重新整理模块
      this.modules = Tools.sortBy(
        Object.values(this.modules),
        (item) => item.constructor.order
      ).reduce(
        (result, item) => ({
          ...result,
          [item.constructor.name]: item,
        }),
        {}
      );
    }

    /**
     * 加载模块列表
     * @param {typeof Module[]} modules 模块列表
     */
    initModules(...modules) {
      // 根据依赖和附加模块决定初始化的顺序
      Tools.sortBy(
        modules,
        (item) => item.depends.length,
        (item) => item.addons.length
      ).forEach((module) => {
        this.initModule(module);
      });
    }

    /**
     * 添加到过滤列表
     * @param {*} item 绑定的 nFilter
     */
    pushData(item) {
      // 清除掉无效数据
      for (let i = 0; i < this.data.length; ) {
        if (document.body.contains(this.data[i].container) === false) {
          this.data.splice(i, 1);
          continue;
        }

        i += 1;
      }

      // 加入过滤列表
      if (this.data.includes(item) === false) {
        this.data.push(item);
      }
    }

    /**
     * 判断指定 UID 是否是自己
     * @param {Number} uid 用户 ID
     */
    isSelf(uid) {
      return unsafeWindow.__CURRENT_UID === uid;
    }

    /**
     * 获取过滤模式
     * @param {*} item 绑定的 nFilter
     */
    async getFilterMode(item) {
      // 获取链接参数
      const params = new URLSearchParams(location.search);

      // 跳过屏蔽(插件自定义)
      if (params.has("nofilter")) {
        return;
      }

      // 收藏
      if (params.has("favor")) {
        return;
      }

      // 只看某人
      if (params.has("authorid")) {
        return;
      }

      // 跳过自己
      if (this.isSelf(item.uid)) {
        return;
      }

      // 声明结果
      const result = {
        mode: -1,
        reason: ``,
      };

      // 根据模块依次过滤
      for (const module of Object.values(this.modules)) {
        await module.filter(item, result);
      }

      // 写入过滤模式和过滤原因
      item.filterMode = this.settings.getNameByMode(result.mode);
      item.reason = result.reason;

      // 通知各模块过滤结果
      for (const module of Object.values(this.modules)) {
        await module.notify(item, result);
      }

      // 继承模式下返回默认过滤模式
      if (item.filterMode === "继承") {
        return this.settings.defaultFilterMode;
      }

      // 返回结果
      return item.filterMode;
    }

    /**
     * 过滤主题
     * @param {*} item 主题内容,见 commonui.topicArg.data
     */
    filterTopic(item) {
      // 绑定事件
      if (item.nFilter === undefined) {
        // 主题 ID
        const tid = item[8];

        // 主题标题
        const title = item[1];
        const subject = title.innerText;

        // 主题作者
        const author = item[2];
        const uid =
          parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) || 0;
        const username = author.innerText;

        // 增加操作角标
        const action = (() => {
          const anchor = item[2].parentNode;

          const element = this.ui.createElement("DIV", "", {
            style: Object.entries({
              position: "absolute",
              right: 0,
              bottom: 0,
              padding: "6px",
              "clip-path": "polygon(100% 0, 100% 100%, 0 100%)",
            })
              .map(([key, value]) => `${key}: ${value}`)
              .join(";"),
          });

          anchor.style.position = "relative";
          anchor.appendChild(element);

          return element;
        })();

        // 主题容器
        const container = title.closest("tr");

        // 过滤函数
        const execute = async () => {
          // 获取过滤模式
          const filterMode = await this.getFilterMode(item.nFilter);

          // 样式处理
          (() => {
            // 还原样式
            // TODO 应该整体采用 className 来实现
            (() => {
              // 标记模式
              title.style.removeProperty("textDecoration");

              // 遮罩模式
              title.classList.remove("filter-mask");
              author.classList.remove("filter-mask");
            })();

            // 样式处理
            (() => {
              // 标记模式下,主题标记会有删除线标识
              if (filterMode === "标记") {
                title.style.textDecoration = "line-through";
                return;
              }

              // 遮罩模式下,主题和作者会有遮罩样式
              if (filterMode === "遮罩") {
                title.classList.add("filter-mask");
                author.classList.add("filter-mask");
                return;
              }

              // 隐藏模式下,容器会被隐藏
              if (filterMode === "隐藏") {
                container.style.display = "none";
                return;
              }
            })();

            // 非隐藏模式下,恢复显示
            if (filterMode !== "隐藏") {
              container.style.removeProperty("display");
            }
          })();
        };

        // 绑定事件
        item.nFilter = {
          tid,
          pid: 0,
          uid,
          username,
          container,
          title,
          author,
          subject,
          action,
          tags: null,
          execute,
        };

        // 添加至列表
        this.pushData(item.nFilter);
      }

      // 开始过滤
      item.nFilter.execute();
    }

    /**
     * 过滤回复
     * @param {*} item 回复内容,见 commonui.postArg.data
     */
    filterReply(item) {
      // 绑定事件
      if (item.nFilter === undefined) {
        // 主题 ID
        const tid = item.tid;

        // 回复 ID
        const pid = item.pid;

        // 判断是否是楼层
        const isFloor = typeof item.i === "number";

        // 回复容器
        const container = isFloor
          ? item.uInfoC.closest("tr")
          : item.uInfoC.closest(".comment_c");

        // 回复标题
        const title = item.subjectC;
        const subject = title.innerText;

        // 回复内容
        const content = item.contentC;
        const contentBak = content.innerHTML;

        // 回复作者
        const author =
          container.querySelector(".posterInfoLine") || item.uInfoC;
        const uid = parseInt(item.pAid, 10) || 0;
        const username = author.querySelector(".author").innerText;
        const avatar = author.querySelector(".avatar");

        // 找到用户 ID,将其视为操作按钮
        const action = container.querySelector('[name="uid"]');

        // 创建一个元素,用于展示标记列表
        // 贴条和高赞不显示
        const tags = (() => {
          if (isFloor === false) {
            return null;
          }

          const element = document.createElement("div");

          element.className = "filter-tags";

          author.appendChild(element);

          return element;
        })();

        // 过滤函数
        const execute = async () => {
          // 获取过滤模式
          const filterMode = await this.getFilterMode(item.nFilter);

          // 样式处理
          (() => {
            // 还原样式
            // TODO 应该整体采用 className 来实现
            (() => {
              // 标记模式
              if (avatar) {
                avatar.style.removeProperty("display");
              }

              content.innerHTML = contentBak;

              // 遮罩模式
              const caption = container.parentNode.querySelector("CAPTION");

              if (caption) {
                container.parentNode.removeChild(caption);
                container.style.removeProperty("display");
              }
            })();

            // 样式处理
            (() => {
              // 标记模式下,隐藏头像,采用泥潭的折叠样式
              if (filterMode === "标记") {
                if (avatar) {
                  avatar.style.display = "none";
                }

                this.ui.collapse(uid, content, contentBak);
                return;
              }

              // 遮罩模式下,楼层会有遮罩样式
              if (filterMode === "遮罩") {
                const caption = document.createElement("CAPTION");

                if (isFloor) {
                  caption.className = "filter-mask filter-mask-block";
                } else {
                  caption.className = "filter-mask filter-mask-block left";
                  caption.style.width = "47%";
                }

                caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
                caption.onclick = () => {
                  const caption = container.parentNode.querySelector("CAPTION");

                  if (caption) {
                    container.parentNode.removeChild(caption);
                    container.style.removeProperty("display");
                  }
                };

                container.parentNode.insertBefore(caption, container);
                container.style.display = "none";
                return;
              }

              // 隐藏模式下,容器会被隐藏
              if (filterMode === "隐藏") {
                container.style.display = "none";
                return;
              }
            })();

            // 非隐藏模式下,恢复显示
            // 楼层的遮罩模式下仍需隐藏
            if (["遮罩", "隐藏"].includes(filterMode) === false) {
              container.style.removeProperty("display");
            }
          })();

          // 过滤引用
          this.filterQuote(item);
        };

        // 绑定事件
        item.nFilter = {
          tid,
          pid,
          uid,
          username,
          container,
          title,
          author,
          subject,
          content: content.innerText,
          action,
          tags,
          execute,
        };

        // 添加至列表
        this.pushData(item.nFilter);
      }

      // 开始过滤
      item.nFilter.execute();
    }

    /**
     * 过滤引用
     * @param {*} item 回复内容,见 commonui.postArg.data
     */
    filterQuote(item) {
      // 未绑定事件,直接跳过
      if (item.nFilter === undefined) {
        return;
      }

      // 回复内容
      const content = item.contentC;

      // 找到所有引用
      const quotes = content.querySelectorAll(".quote");

      // 处理引用
      [...quotes].map(async (quote) => {
        const uid = (() => {
          const ele = quote.querySelector("a[href^='/nuke.php']");

          if (ele) {
            const res = ele.getAttribute("href").match(/uid=(\S+)/);

            if (res) {
              return parseInt(res[1], 10);
            }
          }

          return 0;
        })();

        const { tid, pid } = (() => {
          const ele = quote.querySelector("[title='快速浏览这个帖子']");

          if (ele) {
            const res = ele
              .getAttribute("onclick")
              .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);

            if (res) {
              return {
                tid: parseInt(res[2], 10),
                pid: parseInt(res[3], 10) || 0,
              };
            }
          }

          return {};
        })();

        // 临时的 nFilter
        const nFilter = {
          uid,
          tid,
          pid,
          subject: "",
          content: quote.innerText,
          action: null,
          tags: null,
        };

        // 获取过滤模式
        const filterMode = await this.getFilterMode(nFilter);

        (() => {
          if (filterMode === "标记") {
            this.ui.collapse(uid, quote, quote.innerHTML);
            return;
          }

          if (filterMode === "遮罩") {
            const source = document.createElement("DIV");

            source.innerHTML = quote.innerHTML;
            source.style.display = "none";

            const caption = document.createElement("CAPTION");

            caption.className = "filter-mask filter-mask-block";

            caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
            caption.onclick = () => {
              quote.removeChild(caption);

              source.style.display = "";
            };

            quote.innerHTML = "";
            quote.appendChild(source);
            quote.appendChild(caption);
            return;
          }

          if (filterMode === "隐藏") {
            quote.innerHTML = "";
            return;
          }
        })();

        // 绑定引用
        item.nFilter.quotes = item.nFilter.quotes || {};
        item.nFilter.quotes[uid] = nFilter.filterMode;
      });
    }
  }

  /**
   * 列表模块
   */
  class ListModule extends Module {
    /**
     * 模块名称
     */
    static name = "list";

    /**
     * 模块标签
     */
    static label = "列表";

    /**
     * 顺序
     */
    static order = 10;

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "内容", ellipsis: true },
        { label: "过滤模式", center: true, width: 1 },
        { label: "原因", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 绑定的 nFilter
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { tid, pid, filterMode, reason } = item;

      // 移除 BR 标签
      item.content = (item.content || "").replace(/<br>/g, "");

      // 内容
      const content = (() => {
        if (pid) {
          return ui.createElement("A", item.content, {
            href: `/read.php?pid=${pid}&nofilter`,
          });
        }

        // 如果有 TID 但没有标题,是引用,采用内容逻辑
        if (item.subject.length === 0) {
          return ui.createElement("A", item.content, {
            href: `/read.php?tid=${tid}&nofilter`,
          });
        }

        return ui.createElement("A", item.subject, {
          href: `/read.php?tid=${tid}&nofilter`,
          title: item.content,
          className: "b nobr",
        });
      })();

      return [content, filterMode, reason];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { tabs, content } = this.ui.views;

      const table = this.ui.createTable(this.columns());

      const tab = this.ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        const list = this.data.filter((item) => {
          return (item.filterMode || "显示") !== "显示";
        });

        Object.values(list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify() {
      // 获取过滤后的数量
      const count = this.data.filter((item) => {
        return (item.filterMode || "显示") !== "显示";
      }).length;

      // 更新菜单文字
      const { ui } = this;
      const { menu } = ui;

      if (menu === null) {
        return;
      }

      if (count) {
        menu.innerHTML = `${ui.constructor.label} <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
      } else {
        menu.innerHTML = `${ui.constructor.label}`;
      }

      // 重新渲染
      // TODO 应该给 table 增加一个判重的逻辑,这样只需要更新过滤后的内容即可
      const { tab } = this.views;

      if (tab.querySelector("A").className === "nobr") {
        this.render(ui.views.content);
      }
    }
  }

  /**
   * 用户模块
   */
  class UserModule extends Module {
    /**
     * 模块名称
     */
    static name = "user";

    /**
     * 模块标签
     */
    static label = "用户";

    /**
     * 顺序
     */
    static order = 20;

    /**
     * 获取列表
     */
    get list() {
      return this.settings.users;
    }

    /**
     * 获取用户
     * @param {Number} uid 用户 ID
     */
    get(uid) {
      // 获取列表
      const list = this.list;

      // 如果存在,则返回信息
      if (list[uid]) {
        return list[uid];
      }

      return null;
    }

    /**
     * 添加用户
     * @param {Number} uid 用户 ID
     */
    add(uid, values) {
      // 获取列表
      const list = this.list;

      // 如果已存在,则返回信息
      if (list[uid]) {
        return list[uid];
      }

      // 写入用户信息
      list[uid] = values;

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回添加的用户
      return values;
    }

    /**
     * 编辑用户
     * @param {Number} uid    用户 ID
     * @param {*}      values 用户信息
     */
    update(uid, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, uid) === false) {
        return null;
      }

      // 获取用户
      const entity = list[uid];

      // 更新用户
      Object.assign(entity, values);

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回编辑的用户
      return entity;
    }

    /**
     * 删除用户
     * @param   {Number}        uid 用户 ID
     * @returns {Object | null}     删除的用户
     */
    remove(uid) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, uid) === false) {
        return null;
      }

      // 获取用户
      const entity = list[uid];

      // 删除用户
      delete list[uid];

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回删除的用户
      return entity;
    }

    /**
     * 格式化
     * @param {Number}             uid  用户 ID
     * @param {String | undefined} name 用户名称
     */
    format(uid, name) {
      if (uid <= 0) {
        return null;
      }

      const { ui } = this;

      const user = this.get(uid);

      if (user) {
        name = user.name;
      }

      const username = name ? "@" + name : "#" + uid;

      return ui.createElement("A", `[${username}]`, {
        className: "b nobr",
        href: `/nuke.php?func=ucp&uid=${uid}`,
      });
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "昵称" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 用户信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, name, filterMode } = item;

      // 昵称
      const user = this.format(id, name);

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          this.update(id, {
            filterMode: newMode,
          });

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(remove);
      })();

      return [user, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content, settings } = ui.views;
      const { add } = settings;

      const table = ui.createTable(this.columns());

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);

      // 删除非激活中的用户
      {
        const list = ui.createElement("DIV", [], {
          style: "white-space: normal;",
        });

        const button = ui.createButton("删除非激活中的用户", () => {
          ui.confirm().then(() => {
            list.innerHTML = "";

            const users = Object.values(this.list);

            const waitingQueue = users.map(
              ({ id }) =>
                () =>
                  this.api.getUserInfo(id).then(({ bit }) => {
                    const activeInfo = commonui.activeInfo(0, 0, bit);
                    const activeType = activeInfo[1];

                    if (["ACTIVED", "LINKED"].includes(activeType)) {
                      return;
                    }

                    list.append(this.format(id));

                    this.remove(id);
                  })
            );

            const queueLength = waitingQueue.length;

            const execute = () => {
              if (waitingQueue.length) {
                const next = waitingQueue.shift();

                button.disabled = true;
                button.innerHTML = `删除非激活中的用户 (${
                  queueLength - waitingQueue.length
                }/${queueLength})`;

                next().finally(execute);
                return;
              }

              button.disabled = false;
            };

            execute();
          });
        });

        const element = ui.createElement("DIV", [button, list]);

        add(this.constructor.order + 0, element);
      }
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 渲染详情
     * @param {Number}             uid      用户 ID
     * @param {String | undefined} name     用户名称
     * @param {Function}           callback 回调函数
     */
    renderDetails(uid, name, callback = () => {}) {
      const { ui, settings } = this;

      // 只允许同时存在一个详情页
      if (this.views.details) {
        if (this.views.details.parentNode) {
          this.views.details.parentNode.removeChild(this.views.details);
        }
      }

      // 获取用户信息
      const user = this.get(uid);

      if (user) {
        name = user.name;
      }

      const title =
        (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;

      const filterMode = user ? user.filterMode : settings.filterModes[0];

      const switchMode = ui.createButton(filterMode, () => {
        const newMode = settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      const buttons = ui.createElement(
        "DIV",
        (() => {
          const remove = user
            ? ui.createButton("删除", () => {
                ui.confirm().then(() => {
                  this.remove(uid);

                  this.views.details._.hide();

                  callback("REMOVE");
                });
              })
            : null;

          const save = ui.createButton("保存", () => {
            if (user === null) {
              const entity = this.add(uid, {
                id: uid,
                name,
                tags: [],
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("ADD", entity);
            } else {
              const entity = this.update(uid, {
                name,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("UPDATE", entity);
            }
          });

          return ui.createButtonGroup(remove, save);
        })(),
        {
          className: "right_",
        }
      );

      const actions = ui.createElement(
        "DIV",
        [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
        {
          style: "margin-top: 10px;",
        }
      );

      const tips = ui.createElement("DIV", TIPS.filterMode, {
        className: "silver",
        style: "margin-top: 10px;",
      });

      const content = ui.createElement("DIV", [actions, tips], {
        style: "width: 80vw",
      });

      // 创建弹出框
      this.views.details = ui.createDialog(null, title, content);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取用户信息
      const user = this.get(item.uid);

      // 没有则跳过
      if (user === null) {
        return;
      }

      // 获取用户过滤模式
      const mode = this.settings.getModeByName(user.filterMode);

      // 不高于当前过滤模式则跳过
      if (mode <= result.mode) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `用户模式: ${user.filterMode}`;
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, username, action } = item;

      // 如果没有 action 组件则跳过
      if (action === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        action.style.display = "none";
        return;
      }

      // 获取当前用户
      const user = this.get(uid);

      // 修改操作按钮文字
      if (action.tagName === "A") {
        action.innerText = "屏蔽";
      } else {
        action.title = "屏蔽";
      }

      // 修改操作按钮颜色
      if (user) {
        action.style.background = "#CB4042";
      } else {
        action.style.background = "#AAA";
      }

      // 绑定事件
      action.onclick = () => {
        this.renderDetails(uid, username);
      };
    }

    /**
     * 重新过滤
     * @param {Number} uid 用户 ID
     */
    reFilter(uid) {
      this.data.forEach((item) => {
        // 如果用户 ID 一致,则重新过滤
        if (item.uid === uid) {
          item.execute();
          return;
        }

        // 如果有引用,也重新过滤
        if (Object.hasOwn(item.quotes || {}, uid)) {
          item.execute();
          return;
        }
      });
    }
  }

  /**
   * 标记模块
   */
  class TagModule extends Module {
    /**
     * 模块名称
     */
    static name = "tag";

    /**
     * 模块标签
     */
    static label = "标记";

    /**
     * 顺序
     */
    static order = 30;

    /**
     * 依赖模块
     */
    static depends = [UserModule];

    /**
     * 依赖的用户模块
     * @returns {UserModule} 用户模块
     */
    get userModule() {
      return this.depends[UserModule.name];
    }

    /**
     * 获取列表
     */
    get list() {
      return this.settings.tags;
    }

    /**
     * 获取标记
     * @param {Number} id   标记 ID
     * @param {String} name 标记名称
     */
    get({ id, name }) {
      // 获取列表
      const list = this.list;

      // 通过 ID 获取标记
      if (list[id]) {
        return list[id];
      }

      // 通过名称获取标记
      if (name) {
        const tag = Object.values(list).find((item) => item.name === name);

        if (tag) {
          return tag;
        }
      }

      return null;
    }

    /**
     * 添加标记
     * @param {String} name 标记名称
     */
    add(name) {
      // 获取对应的标记
      const tag = this.get({ name });

      // 如果标记已存在,则返回标记信息,否则增加标记
      if (tag) {
        return tag;
      }

      // 获取列表
      const list = this.list;

      // ID 为最大值 + 1
      const id = Math.max(...Object.keys(list), 0) + 1;

      // 标记的颜色
      const color = Tools.generateColor(name);

      // 写入标记信息
      list[id] = {
        id,
        name,
        color,
        filterMode: this.settings.filterModes[0],
      };

      // 保存数据
      this.settings.tags = list;

      // 返回添加的标记
      return list[id];
    }

    /**
     * 编辑标记
     * @param {Number} id     标记 ID
     * @param {*}      values 标记信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取标记
      const entity = list[id];

      // 获取相关的用户
      const users = Object.values(this.userModule.list).filter((user) =>
        user.tags.includes(id)
      );

      // 更新标记
      Object.assign(entity, values);

      // 保存数据
      this.settings.tags = list;

      // 重新过滤
      this.reFilter(users);
    }

    /**
     * 删除标记
     * @param {Number} id 标记 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取标记
      const entity = list[id];

      // 获取相关的用户
      const users = Object.values(this.userModule.list).filter((user) =>
        user.tags.includes(id)
      );

      // 删除标记
      delete list[id];

      // 删除相关的用户标记
      users.forEach((user) => {
        const index = user.tags.findIndex((item) => item === id);

        if (index >= 0) {
          user.tags.splice(index, 1);
        }
      });

      // 保存数据
      this.settings.tags = list;

      // 重新过滤
      this.reFilter(users);

      // 返回删除的标记
      return entity;
    }

    /**
     * 格式化
     * @param {Number}             id   标记 ID
     * @param {String | undefined} name 标记名称
     * @param {String | undefined} name 标记颜色
     */
    format(id, name, color) {
      const { ui } = this;

      if (id >= 0) {
        const tag = this.get({ id });

        if (tag) {
          name = tag.name;
          color = tag.color;
        }
      }

      if (name && color) {
        return ui.createElement("B", name, {
          className: "block_txt nobr",
          style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
        });
      }

      return "";
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "标记", width: 1 },
        { label: "列表" },
        { label: "过滤模式", width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, filterMode } = item;

      // 标记
      const tag = this.format(id);

      // 用户列表
      const list = Object.values(this.userModule.list)
        .filter(({ tags }) => tags.includes(id))
        .map(({ id }) => this.userModule.format(id));

      const group = ui.createElement("DIV", list, {
        style: "white-space: normal; display: none;",
      });

      const switchButton = ui.createButton(list.length.toString(), () => {
        if (group.style.display === "none") {
          group.style.removeProperty("display");
        } else {
          group.style.display = "none";
        }
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          this.update(id, {
            filterMode: newMode,
          });

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(remove);
      })();

      return [tag, [switchButton, group], switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content, settings } = ui.views;
      const { add } = settings;

      const table = ui.createTable(this.columns());

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);

      // 删除没有标记的用户
      {
        const button = ui.createButton("删除没有标记的用户", () => {
          ui.confirm().then(() => {
            const users = Object.values(this.userModule.list);

            users.forEach(({ id, tags }) => {
              if (tags.length > 0) {
                return;
              }

              this.userModule.remove(id);
            });
          });
        });

        const element = ui.createElement("DIV", button);

        add(this.constructor.order + 0, element);
      }

      // 删除没有用户的标记
      {
        const button = ui.createButton("删除没有用户的标记", () => {
          ui.confirm().then(() => {
            const items = Object.values(this.list);
            const users = Object.values(this.userModule.list);

            items.forEach(({ id }) => {
              if (users.find(({ tags }) => tags.includes(id))) {
                return;
              }

              this.remove(id);
            });
          });
        });

        const element = ui.createElement("DIV", button);

        add(this.constructor.order + 1, element);
      }
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取用户信息
      const user = this.userModule.get(item.uid);

      // 没有则跳过
      if (user === null) {
        return;
      }

      // 获取用户标记
      const tags = user.tags;

      // 取最高的过滤模式
      // 低于当前的过滤模式则跳过
      let max = result.mode;
      let tag = null;

      for (const id of tags) {
        const entity = this.get({ id });

        if (entity === null) {
          continue;
        }

        // 获取过滤模式
        const mode = this.settings.getModeByName(entity.filterMode);

        if (mode <= max) {
          continue;
        }

        max = mode;
        tag = entity;
      }

      // 没有匹配的则跳过
      if (tag === null) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = max;
      result.reason = `标记: ${tag.name}`;
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, tags } = item;

      // 如果没有 tags 组件则跳过
      if (tags === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        tags.style.display = "none";
        return;
      }

      // 删除旧标记
      [...tags.querySelectorAll("[tid]")].forEach((item) => {
        tags.removeChild(item);
      });

      // 获取当前用户
      const user = this.userModule.get(uid);

      // 如果没有用户,则跳过
      if (user === null) {
        return;
      }

      // 格式化标记
      const items = user.tags.map((id) => {
        const item = this.format(id);

        if (item) {
          item.setAttribute("tid", id);
        }

        return item;
      });

      // 加入组件
      items.forEach((item) => {
        if (item) {
          tags.appendChild(item);
        }
      });
    }

    /**
     * 重新过滤
     * @param {Array} users 用户集合
     */
    reFilter(users) {
      users.forEach((user) => {
        this.userModule.reFilter(user.id);
      });
    }
  }

  /**
   * 关键字模块
   */
  class KeywordModule extends Module {
    /**
     * 模块名称
     */
    static name = "keyword";

    /**
     * 模块标签
     */
    static label = "关键字";

    /**
     * 顺序
     */
    static order = 40;

    /**
     * 获取列表
     */
    get list() {
      return this.settings.keywords;
    }

    /**
     * 获取关键字
     * @param {Number} id 关键字 ID
     */
    get(id) {
      // 获取列表
      const list = this.list;

      // 如果存在,则返回信息
      if (list[id]) {
        return list[id];
      }

      return null;
    }

    /**
     * 添加关键字
     * @param {String} keyword     关键字
     * @param {String} filterMode  过滤模式
     * @param {Number} filterLevel 过滤等级: 0 - 仅过滤标题; 1 - 过滤标题和内容
     */
    add(keyword, filterMode, filterLevel) {
      // 获取列表
      const list = this.list;

      // ID 为最大值 + 1
      const id = Math.max(...Object.keys(list), 0) + 1;

      // 写入关键字信息
      list[id] = {
        id,
        keyword,
        filterMode,
        filterLevel,
      };

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();

      // 返回添加的关键字
      return list[id];
    }

    /**
     * 编辑关键字
     * @param {Number} id     关键字 ID
     * @param {*}      values 关键字信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取关键字
      const entity = list[id];

      // 更新关键字
      Object.assign(entity, values);

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();
    }

    /**
     * 删除关键字
     * @param {Number} id 关键字 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取关键字
      const entity = list[id];

      // 删除关键字
      delete list[id];

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();

      // 返回删除的关键字
      return entity;
    }

    /**
     * 获取帖子数据
     * @param {*} item 绑定的 nFilter
     */
    async getPostInfo(item) {
      const { tid, pid } = item;

      // 请求帖子数据
      const { subject, content, userInfo, reputation } =
        await this.api.getPostInfo(tid, pid);

      // 绑定用户信息和声望
      if (userInfo) {
        item.userInfo = userInfo;
        item.username = userInfo.username;
        item.reputation = reputation;
      }

      // 绑定标题和内容
      item.subject = subject;
      item.content = content;
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "关键字" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "包括内容", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, keyword, filterLevel, filterMode } = item;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
        value: keyword,
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 包括内容
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: filterLevel > 0,
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(id, {
            keyword: input.value,
            filterMode: switchMode.innerText,
            filterLevel: switchLevel.checked ? 1 : 0,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [inputWrapper, switchMode, switchLevel, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.keyword, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });

        this.renderNewLine();
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 包括内容
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", (e) => {
          const entity = this.add(
            input.value,
            switchMode.innerText,
            switchLevel.checked ? 1 : 0
          );

          table.update(e, ...this.column(entity));

          this.renderNewLine();
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(inputWrapper, switchMode, switchLevel, buttons);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取列表
      const list = this.list;

      // 跳过低于当前的过滤模式
      const filtered = Object.values(list).filter(
        (item) => this.settings.getModeByName(item.filterMode) > result.mode
      );

      // 没有则跳过
      if (filtered.length === 0) {
        return;
      }

      // 根据过滤模式依次判断
      const sorted = Tools.sortBy(filtered, (item) =>
        this.settings.getModeByName(item.filterMode)
      );

      for (let i = 0; i < sorted.length; i += 1) {
        const { keyword, filterMode } = sorted[i];

        // 过滤等级,0 为只过滤标题,1 为过滤标题和内容
        const filterLevel = sorted[i].filterLevel || 0;

        // 过滤标题
        if (filterLevel >= 0) {
          const { subject } = item;

          const match = subject.match(keyword);

          if (match) {
            const mode = this.settings.getModeByName(filterMode);

            // 更新过滤模式和原因
            result.mode = mode;
            result.reason = `关键字: ${match[0]}`;
            return;
          }
        }

        // 过滤内容
        if (filterLevel >= 1) {
          // 如果没有内容,则请求
          if (item.content === undefined) {
            await this.getPostInfo(item);
          }

          const { content } = item;

          const match = content.match(keyword);

          if (match) {
            const mode = this.settings.getModeByName(filterMode);

            // 更新过滤模式和原因
            result.mode = mode;
            result.reason = `关键字: ${match[0]}`;
            return;
          }
        }
      }
    }

    /**
     * 重新过滤
     */
    reFilter() {
      // 实际上应该根据过滤模式来筛选要过滤的部分
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 属地模块
   */
  class LocationModule extends Module {
    /**
     * 模块名称
     */
    static name = "location";

    /**
     * 模块标签
     */
    static label = "属地";

    /**
     * 顺序
     */
    static order = 50;

    /**
     * 请求缓存
     */
    cache = {};

    /**
     * 获取列表
     */
    get list() {
      return this.settings.locations;
    }

    /**
     * 获取属地
     * @param {Number} id 属地 ID
     */
    get(id) {
      // 获取列表
      const list = this.list;

      // 如果存在,则返回信息
      if (list[id]) {
        return list[id];
      }

      return null;
    }

    /**
     * 添加属地
     * @param {String} keyword     关键字
     * @param {String} filterMode  过滤模式
     */
    add(keyword, filterMode) {
      // 获取列表
      const list = this.list;

      // ID 为最大值 + 1
      const id = Math.max(...Object.keys(list), 0) + 1;

      // 写入属地信息
      list[id] = {
        id,
        keyword,
        filterMode,
      };

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();

      // 返回添加的属地
      return list[id];
    }

    /**
     * 编辑属地
     * @param {Number} id     属地 ID
     * @param {*}      values 属地信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取属地
      const entity = list[id];

      // 更新属地
      Object.assign(entity, values);

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();
    }

    /**
     * 删除属地
     * @param {Number} id 属地 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取属地
      const entity = list[id];

      // 删除属地
      delete list[id];

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();

      // 返回删除的属地
      return entity;
    }

    /**
     * 获取 IP 属地
     * @param {*} item 绑定的 nFilter
     */
    async getIpLocation(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return;
      }

      // 如果已有缓存,直接返回
      if (Object.hasOwn(this.cache, uid)) {
        return this.cache[uid];
      }

      // 请求属地
      const { ipLoc } = await this.api.getUserInfo(uid);

      // 写入缓存
      if (ipLoc) {
        this.cache[uid] = ipLoc;
      }

      // 返回结果
      return ipLoc;
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "关键字" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, keyword, filterMode } = item;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
        value: keyword,
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(id, {
            keyword: input.value,
            filterMode: switchMode.innerText,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [inputWrapper, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.keyword, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });

        this.renderNewLine();
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", (e) => {
          const entity = this.add(input.value, switchMode.innerText);

          table.update(e, ...this.column(entity));

          this.renderNewLine();
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(inputWrapper, switchMode, buttons);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取列表
      const list = this.list;

      // 跳过低于当前的过滤模式
      const filtered = Object.values(list).filter(
        (item) => this.settings.getModeByName(item.filterMode) > result.mode
      );

      // 没有则跳过
      if (filtered.length === 0) {
        return;
      }

      // 获取当前属地
      const location = await this.getIpLocation(item);

      // 请求失败则跳过
      if (location === undefined) {
        return;
      }

      // 根据过滤模式依次判断
      const sorted = Tools.sortBy(filtered, (item) =>
        this.settings.getModeByName(item.filterMode)
      );

      for (let i = 0; i < sorted.length; i += 1) {
        const { keyword, filterMode } = sorted[i];

        const match = location.match(keyword);

        if (match) {
          const mode = this.settings.getModeByName(filterMode);

          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `属地: ${match[0]}`;
          return;
        }
      }
    }

    /**
     * 重新过滤
     */
    reFilter() {
      // 实际上应该根据过滤模式来筛选要过滤的部分
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 猎巫模块
   *
   * 其实是通过 Cache 模块读取配置,而非 Settings
   */
  class HunterModule extends Module {
    /**
     * 模块名称
     */
    static name = "hunter";

    /**
     * 模块标签
     */
    static label = "猎巫";

    /**
     * 顺序
     */
    static order = 60;

    /**
     * 请求缓存
     */
    cache = {};

    /**
     * 请求队列
     */
    queue = [];

    /**
     * 获取列表
     */
    get list() {
      return this.settings.cache
        .get("WITCH_HUNT")
        .then((values) => values || []);
    }

    /**
     * 获取猎巫
     * @param {Number} id 猎巫 ID
     */
    async get(id) {
      // 获取列表
      const list = await this.list;

      // 如果存在,则返回信息
      if (list[id]) {
        return list[id];
      }

      return null;
    }

    /**
     * 添加猎巫
     * @param {Number} fid         版面 ID
     * @param {String} label       标签
     * @param {String} filterMode  过滤模式
     * @param {Number} filterLevel 过滤等级: 0 - 仅标记; 1 - 标记并过滤
     */
    async add(fid, label, filterMode, filterLevel) {
      // FID 只能是数字
      fid = parseInt(fid, 10);

      // 获取列表
      const list = await this.list;

      // 如果版面 ID 已存在,则提示错误
      if (Object.keys(list).includes(fid)) {
        alert("已有相同版面ID");
        return;
      }

      // 请求版面信息
      const info = await this.api.getForumInfo(fid);

      // 如果版面不存在,则提示错误
      if (info === null) {
        alert("版面ID有误");
        return;
      }

      // 计算标记颜色
      const color = Tools.generateColor(info.name);

      // 写入猎巫信息
      list[fid] = {
        fid,
        name: info.name,
        label,
        color,
        filterMode,
        filterLevel,
      };

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤
      this.reFilter(true);

      // 返回添加的猎巫
      return list[fid];
    }

    /**
     * 编辑猎巫
     * @param {Number} fid    版面 ID
     * @param {*}      values 猎巫信息
     */
    async update(fid, values) {
      // 获取列表
      const list = await this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, fid) === false) {
        return null;
      }

      // 获取猎巫
      const entity = list[fid];

      // 更新猎巫
      Object.assign(entity, values);

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤,更新样式即可
      this.reFilter(false);
    }

    /**
     * 删除猎巫
     * @param {Number} fid 版面 ID
     */
    async remove(fid) {
      // 获取列表
      const list = await this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, fid) === false) {
        return null;
      }

      // 获取猎巫
      const entity = list[fid];

      // 删除猎巫
      delete list[fid];

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤
      this.reFilter(true);

      // 返回删除的属地
      return entity;
    }

    /**
     * 格式化版面
     * @param {Number} fid  版面 ID
     * @param {String} name 版面名称
     */
    formatForum(fid, name) {
      const { ui } = this;

      return ui.createElement("A", `[${name}]`, {
        className: "b nobr",
        href: `/thread.php?fid=${fid}`,
      });
    }

    /**
     * 格式化标签
     * @param {String} name 标签名称
     * @param {String} name 标签颜色
     */
    formatLabel(name, color) {
      const { ui } = this;

      return ui.createElement("B", name, {
        className: "block_txt nobr",
        style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
      });
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "版面", width: 200 },
        { label: "标签" },
        { label: "启用过滤", center: true, width: 1 },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { fid, name, label, color, filterMode, filterLevel } = item;

      // 版面
      const forum = this.formatForum(fid, name);

      // 标签
      const labelElement = this.formatLabel(label, color);

      // 启用过滤
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: filterLevel > 0,
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(fid, {
            filterMode: switchMode.innerText,
            filterLevel: switchLevel.checked ? 1 : 0,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(async () => {
            await this.remove(fid);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [forum, labelElement, switchLevel, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.hunter, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        this.list.then((values) => {
          Object.values(values).forEach((item) => {
            const column = this.column(item);

            add(...column);
          });

          this.renderNewLine();
        });
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 版面 ID
      const forumInput = ui.createElement("INPUT", [], {
        type: "text",
      });

      const forumInputWrapper = ui.createElement("DIV", forumInput, {
        className: "filter-input-wrapper",
      });

      // 标签
      const labelInput = ui.createElement("INPUT", [], {
        type: "text",
      });

      const labelInputWrapper = ui.createElement("DIV", labelInput, {
        className: "filter-input-wrapper",
      });

      // 启用过滤
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", async (e) => {
          const entity = await this.add(
            forumInput.value,
            labelInput.value,
            switchMode.innerText,
            switchLevel.checked ? 1 : 0
          );

          table.update(e, ...this.column(entity));

          this.renderNewLine();
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(
        forumInputWrapper,
        labelInputWrapper,
        switchLevel,
        switchMode,
        buttons
      );
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取当前猎巫结果
      const hunter = item.hunter || [];

      // 如果没有猎巫结果,则跳过
      if (hunter.length === 0) {
        return;
      }

      // 获取列表
      const items = await this.list;

      // 筛选出匹配的猎巫
      const list = Object.values(items).filter(({ fid }) =>
        hunter.includes(fid)
      );

      // 取最高的过滤模式
      // 低于当前的过滤模式则跳过
      let max = result.mode;
      let res = null;

      for (const entity of list) {
        const { filterLevel, filterMode } = entity;

        // 仅标记
        if (filterLevel === 0) {
          continue;
        }

        // 获取过滤模式
        const mode = this.settings.getModeByName(filterMode);

        if (mode <= max) {
          continue;
        }

        max = mode;
        res = entity;
      }

      // 没有匹配的则跳过
      if (res === null) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = max;
      result.reason = `猎巫: ${res.label}`;
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, tags } = item;

      // 如果没有 tags 组件则跳过
      if (tags === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        tags.style.display = "none";
        return;
      }

      // 删除旧标签
      [...tags.querySelectorAll("[fid]")].forEach((item) => {
        tags.removeChild(item);
      });

      // 如果没有请求,开始请求
      if (Object.hasOwn(item, "hunter") === false) {
        this.execute(item);
        return;
      }

      // 获取当前猎巫结果
      const hunter = item.hunter;

      // 如果没有猎巫结果,则跳过
      if (hunter.length === 0) {
        return;
      }

      // 格式化标签
      const items = await Promise.all(
        hunter.map(async (fid) => {
          const item = await this.get(fid);

          if (item) {
            const element = this.formatLabel(item.label, item.color);

            element.setAttribute("fid", fid);

            return element;
          }

          return null;
        })
      );

      // 加入组件
      items.forEach((item) => {
        if (item) {
          tags.appendChild(item);
        }
      });
    }

    /**
     * 重新过滤
     * @param {Boolean} clear 是否清除缓存
     */
    reFilter(clear) {
      // 清除缓存
      if (clear) {
        this.cache = {};
      }

      // 重新过滤
      this.data.forEach((item) => {
        // 不需要清除缓存的话,只要重新加载标记
        if (clear === false) {
          item.hunter = [];
        }

        // 重新猎巫
        this.execute(item);
      });
    }

    /**
     * 猎巫
     * @param {*} item 绑定的 nFilter
     */
    async execute(item) {
      const { uid } = item;
      const { api, cache, queue, list } = this;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 初始化猎巫结果,用于标识正在猎巫
      item.hunter = item.hunter || [];

      // 获取列表
      const items = await list;

      // 没有设置且没有旧数据,直接跳过
      if (items.length === 0 && item.hunter.length === 0) {
        return;
      }

      // 重新过滤
      const reload = (newValue) => {
        const isEqual = newValue.sort().join() === item.hunter.sort().join();

        if (isEqual) {
          return;
        }

        item.hunter = newValue;
        item.execute();
      };

      // 创建任务
      const task = async () => {
        // 如果缓存里没有记录,请求数据并写入缓存
        if (Object.hasOwn(cache, uid) === false) {
          cache[uid] = [];

          await Promise.all(
            Object.keys(items).map(async (fid) => {
              // 转换为数字格式
              const id = parseInt(fid, 10);

              // 当前版面发言记录
              const result = await api.getForumPosted(id, uid);

              // 写入当前设置
              if (result) {
                cache[uid].push(id);
              }
            })
          );
        }

        // 重新过滤
        reload(cache[uid]);

        // 将当前任务移出队列
        queue.shift();

        // 如果还有任务,继续执行
        if (queue.length > 0) {
          queue[0]();
        }
      };

      // 队列里已经有任务
      const isRunning = queue.length > 0;

      // 加入队列
      queue.push(task);

      // 如果没有正在执行的任务,则立即执行
      if (isRunning === false) {
        task();
      }
    }
  }

  /**
   * 杂项模块
   */
  class MiscModule extends Module {
    /**
     * 模块名称
     */
    static name = "misc";

    /**
     * 模块标签
     */
    static label = "杂项";

    /**
     * 顺序
     */
    static order = 100;

    /**
     * 请求缓存
     */
    cache = {
      topicNums: {},
    };

    /**
     * 获取用户信息(从页面上)
     * @param {*} item 绑定的 nFilter
     */
    getUserInfo(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return;
      }

      // 回复页面可以直接获取到用户信息和声望
      if (commonui.userInfo) {
        // 取得用户信息
        const userInfo = commonui.userInfo.users[uid];

        // 绑定用户信息和声望
        if (userInfo) {
          item.userInfo = userInfo;
          item.username = userInfo.username;

          item.reputation = (() => {
            const reputations = commonui.userInfo.reputations;

            if (reputations) {
              for (let fid in reputations) {
                return reputations[fid][uid] || 0;
              }
            }

            return NaN;
          })();
        }
      }
    }

    /**
     * 获取帖子数据
     * @param {*} item 绑定的 nFilter
     */
    async getPostInfo(item) {
      const { tid, pid } = item;

      // 请求帖子数据
      const { subject, content, userInfo, reputation } =
        await this.api.getPostInfo(tid, pid);

      // 绑定用户信息和声望
      if (userInfo) {
        item.userInfo = userInfo;
        item.username = userInfo.username;
        item.reputation = reputation;
      }

      // 绑定标题和内容
      item.subject = subject;
      item.content = content;
    }

    /**
     * 获取主题数量
     * @param {*} item 绑定的 nFilter
     */
    async getTopicNum(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return;
      }

      // 如果已有缓存,直接返回
      if (Object.hasOwn(this.cache.topicNums, uid)) {
        return this.cache.topicNums[uid];
      }

      // 请求数量
      const number = await this.api.getTopicNum(uid);

      // 写入缓存
      this.cache.topicNums[uid] = number;

      // 返回结果
      return number;
    }

    /**
     * 初始化,增加设置
     */
    initComponents() {
      super.initComponents();

      const { settings, ui } = this;
      const { add } = ui.views.settings;

      // 小号过滤(注册时间)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterRegdateLimit / 86400000,
          maxLength: 4,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10) || 0;

          if (newValue < 0) {
            return;
          }

          settings.filterRegdateLimit = newValue * 86400000;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏注册时间小于",
          input,
          "天的用户",
          button,
        ]);

        add(this.constructor.order + 0, element);
      }

      // 小号过滤(发帖数)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterPostnumLimit,
          maxLength: 5,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10) || 0;

          if (newValue < 0) {
            return;
          }

          settings.filterPostnumLimit = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏发帖数量小于",
          input,
          "贴的用户",
          button,
        ]);

        add(this.constructor.order + 1, element);
      }

      // 流量号过滤(主题比例)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterTopicRateLimit,
          maxLength: 3,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10) || 100;

          if (newValue <= 0 || newValue > 100) {
            return;
          }

          settings.filterTopicRateLimit = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏发帖比例大于",
          input,
          "%的用户",
          button,
        ]);

        add(this.constructor.order + 2, element);
      }

      // 声望过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterReputationLimit || "",
          maxLength: 4,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10);

          settings.filterReputationLimit = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏版面声望低于",
          input,
          "点的用户",
          button,
        ]);

        add(this.constructor.order + 3, element);
      }

      // 匿名过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "checkbox",
          checked: settings.filterAnonymous,
        });

        const label = ui.createElement("LABEL", ["隐藏匿名的用户", input], {
          style: "display: flex;",
        });

        const element = ui.createElement("DIV", label);

        input.onchange = () => {
          settings.filterAnonymous = input.checked;

          this.reFilter();
        };

        add(this.constructor.order + 4, element);
      }
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 匿名过滤
      await this.filterByAnonymous(item, result);

      // 注册时间过滤
      await this.filterByRegdate(item, result);

      // 发帖数量过滤
      await this.filterByPostnum(item, result);

      // 发帖比例过滤
      await this.filterByTopicRate(item, result);

      // 版面声望过滤
      await this.filterByReputation(item, result);
    }

    /**
     * 根据匿名过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByAnonymous(item, result) {
      const { uid } = item;

      // 如果不是匿名,则跳过
      if (uid > 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取过滤匿名设置
      const filterAnonymous = this.settings.filterAnonymous;

      if (filterAnonymous) {
        // 更新过滤模式和原因
        result.mode = mode;
        result.reason = "匿名";
      }
    }

    /**
     * 根据注册时间过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByRegdate(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取注册时间限制
      const filterRegdateLimit = this.settings.filterRegdateLimit;

      // 未启用则跳过
      if (filterRegdateLimit <= 0) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取注册时间
      const { regdate } = item.userInfo || {};

      // 获取失败则跳过
      if (regdate === undefined) {
        return;
      }

      // 转换时间格式,泥潭接口只精确到秒
      const date = new Date(regdate * 1000);

      // 判断是否符合条件
      if (Date.now() - date > filterRegdateLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `注册时间: ${date.toLocaleDateString()}`;
    }

    /**
     * 根据发帖数量过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByPostnum(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取发帖数量限制
      const filterPostnumLimit = this.settings.filterPostnumLimit;

      // 未启用则跳过
      if (filterPostnumLimit <= 0) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取发帖数量
      const { postnum } = item.userInfo || {};

      // 获取失败则跳过
      if (postnum === undefined) {
        return;
      }

      // 判断是否符合条件
      if (postnum >= filterPostnumLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `发帖数量: ${postnum}`;
    }

    /**
     * 根据发帖比例过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByTopicRate(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取发帖比例限制
      const filterTopicRateLimit = this.settings.filterTopicRateLimit;

      // 未启用则跳过
      if (filterTopicRateLimit <= 0 || filterTopicRateLimit >= 100) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取发帖数量
      const { postnum } = item.userInfo || {};

      // 获取失败则跳过
      if (postnum === undefined) {
        return;
      }

      // 获取主题数量
      const topicNum = await this.getTopicNum(item);

      // 计算发帖比例
      const topicRate = Math.ceil((topicNum / postnum) * 100);

      // 判断是否符合条件
      if (topicRate < filterTopicRateLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `发帖比例: ${topicRate}% (${topicNum}/${postnum})`;
    }

    /**
     * 根据版面声望过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByReputation(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取版面声望限制
      const filterReputationLimit = this.settings.filterReputationLimit;

      // 未启用则跳过
      if (Number.isNaN(filterReputationLimit)) {
        return;
      }

      // 没有声望信息,优先从页面上获取
      if (item.reputation === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.reputation === undefined) {
        await this.getPostInfo(item);
      }

      // 获取版面声望
      const reputation = item.reputation || 0;

      // 判断是否符合条件
      if (reputation >= filterReputationLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `版面声望: ${reputation}`;
    }

    /**
     * 重新过滤
     */
    reFilter() {
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 设置模块
   */
  class SettingsModule extends Module {
    /**
     * 模块名称
     */
    static name = "settings";

    /**
     * 顺序
     */
    static order = 0;

    /**
     * 创建实例
     * @param   {Settings}      settings 设置
     * @param   {API}           api      API
     * @param   {UI}            ui       UI
     * @param   {Array}         data     过滤列表
     * @returns {Module | null}          成功后返回模块实例
     */
    static create(settings, api, ui, data) {
      // 读取设置里的模块列表
      const modules = settings.modules;

      // 如果不包含自己,加入列表中,因为设置模块是必须的
      if (modules.includes(this.name) === false) {
        settings.modules = [...modules, this.name];
      }

      // 创建实例
      return super.create(settings, api, ui, data);
    }

    /**
     * 初始化,增加设置
     */
    initComponents() {
      super.initComponents();

      const { settings, ui } = this;
      const { add } = ui.views.settings;

      // 前置过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "checkbox",
        });

        const label = ui.createElement("LABEL", ["前置过滤", input], {
          style: "display: flex;",
        });

        settings.preFilterEnabled.then((checked) => {
          input.checked = checked;
          input.onchange = () => {
            settings.preFilterEnabled = !checked;
          };
        });

        add(this.constructor.order + 0, label);
      }

      // 模块选择
      {
        const modules = [
          ListModule,
          UserModule,
          TagModule,
          KeywordModule,
          LocationModule,
          HunterModule,
          MiscModule,
        ];

        const items = modules.map((item) => {
          const input = ui.createElement("INPUT", [], {
            type: "checkbox",
            value: item.name,
            checked: settings.modules.includes(item.name),
            onchange: () => {
              const checked = input.checked;

              modules.map((m, index) => {
                const isDepend = checked
                  ? item.depends.find((i) => i.name === m.name)
                  : m.depends.find((i) => i.name === item.name);

                if (isDepend) {
                  const element = items[index].querySelector("INPUT");

                  if (element) {
                    element.checked = checked;
                  }
                }
              });
            },
          });

          const label = ui.createElement("LABEL", [item.label, input], {
            style: "display: flex; margin-right: 10px;",
          });

          return label;
        });

        const button = ui.createButton("确认", () => {
          const checked = group.querySelectorAll("INPUT:checked");
          const values = [...checked].map((item) => item.value);

          settings.modules = values;

          location.reload();
        });

        const group = ui.createElement("DIV", [...items, button], {
          style: "display: flex;",
        });

        const label = ui.createElement("LABEL", "启用模块");

        add(this.constructor.order + 1, label, group);
      }

      // 默认过滤模式
      {
        const modes = ["标记", "遮罩", "隐藏"].map((item) => {
          const input = ui.createElement("INPUT", [], {
            type: "radio",
            name: "defaultFilterMode",
            value: item,
            checked: settings.defaultFilterMode === item,
            onchange: () => {
              settings.defaultFilterMode = item;

              this.reFilter();
            },
          });

          const label = ui.createElement("LABEL", [item, input], {
            style: "display: flex; margin-right: 10px;",
          });

          return label;
        });

        const group = ui.createElement("DIV", modes, {
          style: "display: flex;",
        });

        const label = ui.createElement("LABEL", "默认过滤模式");

        const tips = ui.createElement("DIV", TIPS.filterMode, {
          className: "silver",
        });

        add(this.constructor.order + 2, label, group, tips);
      }
    }

    /**
     * 重新过滤
     */
    reFilter() {
      // 目前仅在修改默认过滤模式时重新过滤
      this.data.forEach((item) => {
        // 如果过滤模式是继承,则重新过滤
        if (item.filterMode === "继承") {
          item.execute();
        }

        // 如果有引用,也重新过滤
        if (Object.values(item.quotes || {}).includes("继承")) {
          item.execute();
          return;
        }
      });
    }
  }

  /**
   * 增强的列表模块,增加了用户作为附加模块
   */
  class ListEnhancedModule extends ListModule {
    /**
     * 模块名称
     */
    static name = "list";

    /**
     * 附加模块
     */
    static addons = [UserModule];

    /**
     * 附加的用户模块
     * @returns {UserModule} 用户模块
     */
    get userModule() {
      return this.addons[UserModule.name];
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      const hasAddon = this.hasAddon(UserModule);

      if (hasAddon === false) {
        return super.columns();
      }

      return [
        { label: "用户", width: 1 },
        { label: "内容", ellipsis: true },
        { label: "过滤模式", center: true, width: 1 },
        { label: "原因", width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 绑定的 nFilter
     * @returns {Array}      表格项集合
     */
    column(item) {
      const column = super.column(item);

      const hasAddon = this.hasAddon(UserModule);

      if (hasAddon === false) {
        return column;
      }

      const { ui } = this;
      const { table } = this.views;
      const { uid, username } = item;

      const user = this.userModule.format(uid, username);

      const buttons = (() => {
        if (uid <= 0) {
          return null;
        }

        const block = ui.createButton("屏蔽", (e) => {
          this.userModule.renderDetails(uid, username, (type) => {
            // 删除失效数据,等待重新过滤
            table.remove(e);

            // 如果是新增,不会因为用户重新过滤,需要主动触发
            if (type === "ADD") {
              this.userModule.reFilter(uid);
            }
          });
        });

        return ui.createButtonGroup(block);
      })();

      return [user, ...column, buttons];
    }
  }

  /**
   * 增强的用户模块,增加了标记作为附加模块
   */
  class UserEnhancedModule extends UserModule {
    /**
     * 模块名称
     */
    static name = "user";

    /**
     * 附加模块
     */
    static addons = [TagModule];

    /**
     * 附加的标记模块
     * @returns {TagModule} 标记模块
     */
    get tagModule() {
      return this.addons[TagModule.name];
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return super.columns();
      }

      return [
        { label: "昵称", width: 1 },
        { label: "标记" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 用户信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const column = super.column(item);

      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return column;
      }

      const { ui } = this;
      const { table } = this.views;
      const { id, name } = item;

      const tags = ui.createElement(
        "DIV",
        item.tags.map((id) => this.tagModule.format(id))
      );

      const newColumn = [...column];

      newColumn.splice(1, 0, tags);

      const buttons = column[column.length - 1];

      const update = ui.createButton("编辑", (e) => {
        this.renderDetails(id, name, (type, newValue) => {
          if (type === "UPDATE") {
            table.update(e, ...this.column(newValue));
          }

          if (type === "REMOVE") {
            table.remove(e);
          }
        });
      });

      buttons.insertBefore(update, buttons.firstChild);

      return newColumn;
    }

    /**
     * 渲染详情
     * @param {Number}             uid      用户 ID
     * @param {String | undefined} name     用户名称
     * @param {Function}           callback 回调函数
     */
    renderDetails(uid, name, callback = () => {}) {
      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return super.renderDetails(uid, name, callback);
      }

      const { ui, settings } = this;

      // 只允许同时存在一个详情页
      if (this.views.details) {
        if (this.views.details.parentNode) {
          this.views.details.parentNode.removeChild(this.views.details);
        }
      }

      // 获取用户信息
      const user = this.get(uid);

      if (user) {
        name = user.name;
      }

      // TODO 需要优化

      const title =
        (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;

      const table = ui.createTable([]);

      {
        const size = Math.floor((screen.width * 0.8) / 200);

        const items = Object.values(this.tagModule.list).map(({ id }) => {
          const checked = user && user.tags.includes(id) ? "checked" : "";

          return `
            <td class="c1">
              <label for="s-tag-${id}" style="display: block; cursor: pointer;">
                ${this.tagModule.format(id).outerHTML}
              </label>
            </td>
            <td class="c2" width="1">
              <input id="s-tag-${id}" type="checkbox" value="${id}" ${checked}/>
            </td>
          `;
        });

        const rows = [...new Array(Math.ceil(items.length / size))].map(
          (_, index) => `
            <tr class="row${(index % 2) + 1}">
              ${items.slice(size * index, size * (index + 1)).join("")}
            </tr>
          `
        );

        table.querySelector("TBODY").innerHTML = rows.join("");
      }

      const input = ui.createElement("INPUT", [], {
        type: "text",
        placeholder: TIPS.addTags,
        style: "width: -webkit-fill-available;",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        style: "margin-top: 10px;",
      });

      const filterMode = user ? user.filterMode : settings.filterModes[0];

      const switchMode = ui.createButton(filterMode, () => {
        const newMode = settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      const buttons = ui.createElement(
        "DIV",
        (() => {
          const remove = user
            ? ui.createButton("删除", () => {
                ui.confirm().then(() => {
                  this.remove(uid);

                  this.views.details._.hide();

                  callback("REMOVE");
                });
              })
            : null;

          const save = ui.createButton("保存", () => {
            const checked = [...table.querySelectorAll("INPUT:checked")].map(
              (input) => parseInt(input.value, 10)
            );

            const newTags = input.value
              .split("|")
              .filter((item) => item.length)
              .map((item) => this.tagModule.add(item))
              .filter((tag) => tag !== null)
              .map((tag) => tag.id);

            const tags = [...new Set([...checked, ...newTags])].sort();

            if (user === null) {
              const entity = this.add(uid, {
                id: uid,
                name,
                tags,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("ADD", entity);
            } else {
              const entity = this.update(uid, {
                name,
                tags,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("UPDATE", entity);
            }
          });

          return ui.createButtonGroup(remove, save);
        })(),
        {
          className: "right_",
        }
      );

      const actions = ui.createElement(
        "DIV",
        [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
        {
          style: "margin-top: 10px;",
        }
      );

      const tips = ui.createElement("DIV", TIPS.filterMode, {
        className: "silver",
        style: "margin-top: 10px;",
      });

      const content = ui.createElement(
        "DIV",
        [table, inputWrapper, actions, tips],
        {
          style: "width: 80vw",
        }
      );

      // 创建弹出框
      this.views.details = ui.createDialog(null, title, content);
    }
  }

  /**
   * 处理 topicArg 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui.topicArg
   */
  const handleTopicModule = async (filter, value) => {
    // 绑定主题模块
    topicModule = value;

    // 是否启用前置过滤
    const preFilterEnabled = await filter.settings.preFilterEnabled;

    // 前置过滤
    // 先直接隐藏,等过滤完毕后再放出来
    const beforeGet = (...args) => {
      if (preFilterEnabled) {
        // 主题标题
        const title = document.getElementById(args[1]);

        // 主题容器
        const container = title.closest("tr");

        // 隐藏元素
        container.style.display = "none";
      }

      return args;
    };

    // 过滤
    const afterGet = (_, args) => {
      // 主题 ID
      const tid = args[8];

      // 回复 ID
      const pid = args[9];

      // 找到对应数据
      const data = topicModule.data.find(
        (item) => item[8] === tid && item[9] === pid
      );

      // 开始过滤
      if (data) {
        filter.filterTopic(data);
      }
    };

    // 如果已经有数据,则直接过滤
    Object.values(topicModule.data).forEach(filter.filterTopic);

    // 拦截 add 函数,这是泥潭的主题添加事件
    Tools.interceptProperty(topicModule, "add", {
      beforeGet,
      afterGet,
    });
  };

  /**
   * 处理 postArg 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui.postArg
   */
  const handleReplyModule = async (filter, value) => {
    // 绑定回复模块
    replyModule = value;

    // 是否启用前置过滤
    const preFilterEnabled = await filter.settings.preFilterEnabled;

    // 前置过滤
    // 先直接隐藏,等过滤完毕后再放出来
    const beforeGet = (...args) => {
      if (preFilterEnabled) {
        // 楼层号
        const index = args[0];

        // 判断是否是楼层
        const isFloor = typeof index === "number";

        // 评论额外标签
        const prefix = isFloor ? "" : "comment";

        // 用户容器
        const uInfoC = document.querySelector(`#${prefix}posterinfo${index}`);

        // 回复容器
        const container = isFloor
          ? uInfoC.closest("tr")
          : uInfoC.closest(".comment_c");

        // 隐藏元素
        container.style.display = "none";
      }

      return args;
    };

    // 过滤
    const afterGet = (_, args) => {
      // 楼层号
      const index = args[0];

      // 找到对应数据
      const data = replyModule.data[index];

      // 开始过滤
      if (data) {
        filter.filterReply(data);
      }
    };

    // 如果已经有数据,则直接过滤
    Object.values(replyModule.data).forEach(filter.filterReply);

    // 拦截 proc 函数,这是泥潭的回复添加事件
    Tools.interceptProperty(replyModule, "proc", {
      beforeGet,
      afterGet,
    });
  };

  /**
   * 处理 commonui 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui
   */
  const handleCommonui = (filter, value) => {
    // 绑定主模块
    commonui = value;

    // 拦截 mainMenu 模块,UI 需要在 init 后加载
    Tools.interceptProperty(commonui, "mainMenu", {
      afterSet: (value) => {
        Tools.interceptProperty(value, "init", {
          afterGet: () => {
            filter.ui.render();
          },
          afterSet: () => {
            filter.ui.render();
          },
        });
      },
    });

    // 拦截 topicArg 模块,这是泥潭的主题入口
    Tools.interceptProperty(commonui, "topicArg", {
      afterSet: (value) => {
        handleTopicModule(filter, value);
      },
    });

    // 拦截 postArg 模块,这是泥潭的回复入口
    Tools.interceptProperty(commonui, "postArg", {
      afterSet: (value) => {
        handleReplyModule(filter, value);
      },
    });
  };

  /**
   * 注册脚本菜单
   * @param {Settings} settings 设置
   */
  const registerMenu = async (settings) => {
    // 修改 UA
    {
      const userAgent = await settings.userAgent;

      GM_registerMenuCommand(`修改UA:${userAgent}`, () => {
        const value = prompt("修改UA", userAgent);

        if (value) {
          settings.userAgent = value;
        }
      });
    }

    // 前置过滤
    {
      const enabled = await settings.preFilterEnabled;

      GM_registerMenuCommand(`前置过滤:${enabled ? "是" : "否"}`, () => {
        settings.preFilterEnabled = !enabled;
      });
    }
  };

  // 主函数
  (async () => {
    // 初始化缓存、设置
    const cache = new Cache(API.modules);
    const settings = new Settings(cache);

    // 读取设置
    await settings.load();

    // 初始化 API、UI
    const api = new API(cache, settings);
    const ui = new UI(settings, api);

    // 初始化过滤器
    const filter = new Filter(settings, api, ui);

    // 加载模块
    filter.initModules(
      SettingsModule,
      ListEnhancedModule,
      UserEnhancedModule,
      TagModule,
      KeywordModule,
      LocationModule,
      HunterModule,
      MiscModule
    );

    // 注册脚本菜单
    registerMenu(settings);

    // 处理 commonui 模块
    if (unsafeWindow.commonui) {
      handleCommonui(filter, unsafeWindow.commonui);
      return;
    }

    Tools.interceptProperty(unsafeWindow, "commonui", {
      afterSet: (value) => {
        handleCommonui(filter, value);
      },
    });
  })();
})();