Greasy Fork is available in English.

NGA Filter

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

От 04.01.2024. Виж последната версия.

// ==UserScript==
// @name        NGA Filter
// @namespace   https://greasyfork.org/users/263018
// @version     2.1.2
// @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, menuModule, 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";

  // User Agent
  const USER_AGENT = (() => {
    const data = GM_getValue(USER_AGENT_KEY) || "Nga_Official";

    GM_registerMenuCommand(`修改UA:${data}`, () => {
      const value = prompt("修改UA", data);

      if (value) {
        GM_setValue(USER_AGENT_KEY, value);

        location.reload();
      }
    });

    return data;
  })();

  // 前置过滤
  const preFilter = (() => {
    const data = GM_getValue(PRE_FILTER_KEY);

    const value = data === undefined ? true : data;

    GM_registerMenuCommand(`前置过滤:${value ? "是" : "否"}`, () => {
      GM_setValue(PRE_FILTER_KEY, !value);

      location.reload();
    });

    return value;
  })();

  // 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;
    }
  `);

  // 重新过滤
  const reFilter = async (skip = () => false) => {
    // 清空列表
    listModule.clear();

    // 开始过滤
    [
      ...(topicModule ? Object.values(topicModule.data) : []),
      ...(replyModule ? Object.values(replyModule.data) : []),
    ].forEach((item) => {
      // 未绑定事件
      if (item.nFilter === undefined) {
        return;
      }

      // 如果跳过过滤,直接添加列表
      if (skip(item.nFilter)) {
        listModule.add(item.nFilter);
        return;
      }

      // 执行过滤
      item.nFilter.execute();
    });
  };

  // 缓存模块
  const cacheModule = (() => {
    // 声明模块集合
    const modules = {};

    // IndexedDB 操作
    const db = (() => {
      // 常量
      const VERSION = 2;
      const DB_NAME = "NGA_FILTER_CACHE";

      // 是否支持
      const support = unsafeWindow.indexedDB !== undefined;

      // 不支持,直接返回
      if (support === false) {
        return {
          support,
        };
      }

      // 创建或获取数据库实例
      const getInstance = (() => {
        let instance;

        return () =>
          new Promise((resolve) => {
            // 如果已存在实例,直接返回
            if (instance) {
              resolve(instance);
              return;
            }

            // 打开 IndexedDB 数据库
            const request = unsafeWindow.indexedDB.open(DB_NAME, VERSION);

            // 如果数据库不存在则创建
            request.onupgradeneeded = (event) => {
              // 获取旧版本号
              var oldVersion = event.oldVersion;

              // 根据版本号创建表
              Object.entries(modules).map(([name, { keyPath, version }]) => {
                if (version > oldVersion) {
                  // 创建表
                  const store = event.target.result.createObjectStore(name, {
                    keyPath,
                  });

                  // 创建索引,用于清除过期数据
                  store.createIndex("timestamp", "timestamp");
                }
              });
            };

            // 成功后写入实例并返回
            request.onsuccess = (event) => {
              instance = event.target.result;

              resolve(instance);
            };
          });
      })();

      return {
        support,
        getInstance,
      };
    })();

    // 删除缓存
    const remove = async (name, key) => {
      // 不支持 IndexedDB,使用 GM_setValue
      if (db.support === false) {
        const cache = GM_getValue(name) || {};

        delete cache[key];

        GM_setValue(name, cache);
        return;
      }

      // 获取实例
      const instance = await db.getInstance();

      // 写入 IndexedDB
      await new Promise((resolve) => {
        // 创建事务
        const transaction = instance.transaction([name], "readwrite");

        // 获取对象仓库
        const store = transaction.objectStore(name);

        // 删除数据
        const r = store.delete(key);

        r.onsuccess = () => {
          resolve();
        };

        r.onerror = () => {
          resolve();
        };
      });
    };

    // 写入缓存
    const save = async (name, key, value) => {
      // 不支持 IndexedDB,使用 GM_setValue
      if (db.support === false) {
        const cache = GM_getValue(name) || {};

        cache[key] = value;

        GM_setValue(name, cache);
        return;
      }

      // 获取实例
      const instance = await db.getInstance();

      // 写入 IndexedDB
      await new Promise((resolve) => {
        // 创建事务
        const transaction = instance.transaction([name], "readwrite");

        // 获取对象仓库
        const store = transaction.objectStore(name);

        // 插入数据
        const r = store.put({
          ...value,
          timestamp: Date.now(),
        });

        r.onsuccess = () => {
          resolve();
        };

        r.onerror = () => {
          resolve();
        };
      });
    };

    // 读取缓存
    const load = async (name, key, expireTime = 0) => {
      // 不支持 IndexedDB,使用 GM_getValue
      if (db.support === false) {
        const cache = GM_getValue(name) || {};

        if (cache[key]) {
          const result = cache[key];

          // 如果已超时则删除
          if (expireTime > 0) {
            if (result.timestamp + expireTime < new Date().getTime()) {
              await remove(name, key);

              return null;
            }
          }

          return result;
        }

        return null;
      }

      // 获取实例
      const instance = await db.getInstance();

      // 查找 IndexedDB
      const result = await new Promise((resolve) => {
        // 创建事务
        const transaction = instance.transaction([name], "readonly");

        // 获取对象仓库
        const store = transaction.objectStore(name);

        // 获取数据
        const request = store.get(key);

        // 成功后处理数据
        request.onsuccess = (event) => {
          const data = event.target.result;

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

          resolve(null);
        };

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

      // 没有数据
      if (result === null) {
        return null;
      }

      // 如果已超时则删除
      if (expireTime > 0) {
        if (result.timestamp + expireTime < new Date().getTime()) {
          await remove(name, key);

          return null;
        }
      }

      // 返回结果
      return result;
    };

    // 定时清理
    const clear = async () => {
      // 获取实例
      const instance = await db.getInstance();

      // 清理 IndexedDB
      Object.entries(modules).map(([name, { persistent }]) => {
        // 持久化,不进行自动清理
        if (persistent) {
          return;
        }

        // 创建事务
        const transaction = instance.transaction([name], "readwrite");

        // 获取对象仓库
        const store = transaction.objectStore(name);

        // 清理数据
        store.clear();
      });
    };

    // 初始化,用于写入表信息
    const init = (name, value) => {
      modules[name] = value;
    };

    return {
      init,
      save,
      load,
      remove,
      clear,
    };
  })();

  // 过滤模块
  const filterModule = (() => {
    // 过滤提示
    const tips =
      "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承";

    // 过滤方式
    const modes = ["继承", "标记", "遮罩", "隐藏", "显示"];

    // 默认过滤方式
    const defaultMode = modes[0];

    // 切换过滤方式
    const switchModeByName = (value) =>
      modes[modes.indexOf(value) + 1] || defaultMode;

    // 获取当前过滤方式下标
    const getModeByName = (name, defaultValue = 0) => {
      const index = modes.indexOf(name);

      if (index < 0) {
        return defaultValue;
      }

      return index;
    };

    // 获取指定下标过滤方式
    const getNameByMode = (index) => modes[index] || "";

    // 折叠样式
    const collapse = (uid, element, content) => {
      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('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
            <div style="display: none;" name="troll_${uid}">
                ${content}
            </div>
        </div>`;
    };

    return {
      tips,
      modes,
      defaultMode,
      collapse,
      getModeByName,
      getNameByMode,
      switchModeByName,
    };
  })();

  // 数据(及配置)模块
  const dataModule = (() => {
    // 合并数据
    const merge = (() => {
      const isObject = (value) => {
        return value !== null && typeof value === "object";
      };

      const deepClone = (value) => {
        if (isObject(value)) {
          const clone = Array.isArray(value) ? [] : {};

          for (const key in value) {
            if (Object.prototype.hasOwnProperty.call(value, key)) {
              clone[key] = deepClone(value[key]);
            }
          }

          return clone;
        }

        return value;
      };

      return (target, ...sources) => {
        for (const source of sources) {
          for (const key in source) {
            if (isObject(source[key])) {
              if (isObject(target[key])) {
                merge(target[key], source[key]);
              } else {
                target[key] = deepClone(source[key]);
              }
            } else {
              target[key] = source[key];
            }
          }
        }

        return target;
      };
    })();

    // 初始化数据
    const data = (() => {
      // 默认配置
      const defaultData = {
        tags: {},
        users: {},
        keywords: {},
        locations: {},
        options: {
          filterRegdateLimit: 0,
          filterPostnumLimit: 0,
          filterTopicRateLimit: 100,
          filterReputationLimit: NaN,
          filterAnony: false,
          filterMode: "隐藏",
        },
      };

      // 读取数据
      const storedData = GM_getValue(DATA_KEY);

      // 如果没有数据,则返回默认配置
      if (typeof storedData !== "object") {
        return defaultData;
      }

      // 返回数据
      return merge(defaultData, storedData);
    })();

    // 保存数据
    const save = (values) => {
      merge(data, values);

      GM_setValue(DATA_KEY, data);
    };

    // 返回标记列表
    const getTags = () => data.tags;

    // 返回用户列表
    const getUsers = () => data.users;

    // 返回关键字列表
    const getKeywords = () => data.keywords;

    // 返回属地列表
    const getLocations = () => data.locations;

    // 获取默认过滤模式
    const getDefaultFilterMode = () => data.options.filterMode;

    // 设置默认过滤模式
    const setDefaultFilterMode = (value) => {
      save({
        options: {
          filterMode: value,
        },
      });
    };

    // 获取注册时间限制
    const getFilterRegdateLimit = () => data.options.filterRegdateLimit || 0;

    // 设置注册时间限制
    const setFilterRegdateLimit = (value) => {
      save({
        options: {
          filterRegdateLimit: value,
        },
      });
    };

    // 获取发帖数量限制
    const getFilterPostnumLimit = () => data.options.filterPostnumLimit || 0;

    // 设置发帖数量限制
    const setFilterPostnumLimit = (value) => {
      save({
        options: {
          filterPostnumLimit: value,
        },
      });
    };

    // 获取发帖比例限制
    const getFilterTopicRateLimit = () =>
      data.options.filterTopicRateLimit || 100;

    // 设置发帖比例限制
    const setFilterTopicRateLimit = (value) => {
      save({
        options: {
          filterTopicRateLimit: value,
        },
      });
    };

    // 获取用户声望限制
    const getFilterReputationLimit = () =>
      data.options.filterReputationLimit || NaN;

    // 设置用户声望限制
    const setFilterReputationLimit = (value) => {
      save({
        options: {
          filterReputationLimit: value,
        },
      });
    };

    // 获取是否过滤匿名
    const getFilterAnony = () => data.options.filterAnony || false;

    // 设置是否过滤匿名
    const setFilterAnony = (value) => {
      save({
        options: {
          filterAnony: value,
        },
      });
    };

    return {
      save,
      getTags,
      getUsers,
      getKeywords,
      getLocations,
      getDefaultFilterMode,
      setDefaultFilterMode,
      getFilterRegdateLimit,
      setFilterRegdateLimit,
      getFilterPostnumLimit,
      setFilterPostnumLimit,
      getFilterTopicRateLimit,
      setFilterTopicRateLimit,
      getFilterReputationLimit,
      setFilterReputationLimit,
      getFilterAnony,
      setFilterAnony,
    };
  })();

  // 列表模块
  const listModule = (() => {
    const list = [];

    const callback = [];

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.innerHTML = `
          <div class="filter-table-wrapper">
            <table class="filter-table forumbox">
              <thead>
                <tr class="block_txt_c0">
                  <th class="c1" width="1">用户</th>
                  <th class="c2" width="1">过滤方式</th>
                  <th class="c3">内容</th>
                  <th class="c4" width="1">原因</th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
        `;

        return element;
      })();

      const tbody = content.querySelector("TBODY");

      const load = (item) => {
        const { uid, username, tid, pid, filterMode, reason } = item;

        // 用户
        const user = userModule.format(uid, username);

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

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

            return `<a href="${`/read.php?tid=${tid}`}&nofilter" title="${
              item.content
            }" class="b nobr">${item.subject}</a>`;
          }

          return item.subject;
        })();

        // 内容
        const content = (() => {
          if (pid) {
            return `<a href="${`/read.php?pid=${pid}`}&nofilter">${
              item.content
            }</a>`;
          }

          return item.content;
        })();

        const row = document.createElement("TR");

        row.className = `row${(tbody.querySelectorAll("TR").length % 2) + 1}`;

        row.innerHTML = `
          <td class="c1">${user}</td>
          <td class="c2">${filterMode}</td>
          <td class="c3">
            <div class="filter-text-ellipsis">
              ${subject || content}
            </div>
          </td>
          <td class="c4">${reason}</td>
        `;

        tbody.insertBefore(row, tbody.firstChild);
      };

      const refresh = () => {
        tbody.innerHTML = "";

        Object.values(list).forEach(load);
      };

      return {
        content,
        refresh,
        load,
      };
    })();

    const add = (value) => {
      if (
        list.find(
          (item) =>
            item.tid === value.tid &&
            item.pid === value.pid &&
            item.subject === value.subject
        )
      ) {
        return;
      }

      if ((value.filterMode || "显示") === "显示") {
        return;
      }

      list.push(value);

      view.load(value);

      callback.forEach((item) => item(list));
    };

    const clear = () => {
      list.splice(0, list.length);

      view.refresh();

      callback.forEach((item) => item(list));
    };

    const bindCallback = (func) => {
      func(list);

      callback.push(func);
    };

    return {
      add,
      clear,
      bindCallback,
      view,
    };
  })();

  // 用户模块
  const userModule = (() => {
    // 获取用户列表
    const list = () => dataModule.getUsers();

    // 获取用户
    const get = (uid) => {
      // 获取列表
      const users = list();

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

      return null;
    };

    // 增加用户
    const add = (uid, username, tags, filterMode) => {
      // 获取对应的用户
      const user = get(uid);

      // 如果用户已存在,则返回用户信息,否则增加用户
      if (user) {
        return user;
      }

      // 保存用户
      // TODO id 和 name 属于历史遗留问题,应该改为 uid 和 username 以便更好的理解
      dataModule.save({
        users: {
          [uid]: {
            id: uid,
            name: username,
            tags,
            filterMode,
          },
        },
      });

      // 返回用户信息
      return get(uid);
    };

    // 编辑用户
    const edit = (uid, values) => {
      dataModule.save({
        users: {
          [uid]: values,
        },
      });
    };

    // 删除用户
    const remove = (uid) => {
      // TODO 这里不可避免的直接操作了原始数据
      delete list()[uid];

      // 保存数据
      dataModule.save({});
    };

    // 格式化用户
    const format = (uid, name) => {
      if (uid <= 0) {
        return "";
      }

      const user = get(uid);

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

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

      return `<a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">[${username}]</a>`;
    };

    // UI
    const view = (() => {
      const details = (() => {
        let window;

        return (uid, name, callback) => {
          if (window === undefined) {
            window = commonui.createCommmonWindow();
          }

          const user = get(uid);

          const content = document.createElement("DIV");

          const size = Math.floor((screen.width * 0.8) / 200);

          const items = Object.values(tagModule.list()).map((tag, index) => {
            const checked = user && user.tags.includes(tag.id) ? "checked" : "";

            return `
              <td class="c1">
                <label for="s-tag-${index}" style="display: block; cursor: pointer;">
                  ${tagModule.format(tag.id)}
                </label>
              </td>
              <td class="c2" width="1">
                <input id="s-tag-${index}" type="checkbox" value="${
              tag.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>
            `
          );

          content.className = "w100";
          content.innerHTML = `
            <div class="filter-table-wrapper" style="width: 80vw;">
              <table class="filter-table forumbox">
                <tbody>
                  ${rows.join("")}
                </tbody>
              </table>
            </div>
            <div style="margin: 10px 0;">
              <input type="text" placeholder="一次性添加多个标记用&quot;|&quot;隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
            </div>
            <div style="margin: 10px 0;">
              <span>过滤方式:</span>
              <button>${
                (user && user.filterMode) || filterModule.defaultMode
              }</button>
              <div class="right_">
                <button>删除</button>
                <button>保存</button>
              </div>
            </div>
            <div class="silver" style="margin-top: 5px;">${
              filterModule.tips
            }</div>
          `;

          const actions = content.querySelectorAll("BUTTON");

          actions[0].onclick = () => {
            actions[0].innerText = filterModule.switchModeByName(
              actions[0].innerText
            );
          };

          actions[1].onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

            remove(uid);
            reFilter((item) => item.uid !== uid);

            if (callback) {
              callback({
                id: null,
              });
            }

            window._.hide();
          };

          actions[2].onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

            const filterMode = actions[0].innerText;

            const checked = [...content.querySelectorAll("INPUT:checked")].map(
              (input) => parseInt(input.value, 10)
            );

            const newTags = content
              .querySelector("INPUT[type='text']")
              .value.split("|")
              .filter((item) => item.length)
              .map((item) => tagModule.add(item));

            const tags = [...new Set([...checked, ...newTags])].sort();

            if (user) {
              user.tags = tags;

              edit(uid, {
                filterMode,
              });
            } else {
              add(uid, name, tags, filterMode);
            }

            reFilter((item) => item.uid !== uid);

            if (callback) {
              callback({
                uid,
                name,
                tags,
                filterMode,
              });
            }

            window._.hide();
          };

          if (user === null) {
            actions[1].style.display = "none";
          }

          window._.addContent(null);
          window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`);
          window._.addContent(content);
          window._.show();
        };
      })();

      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.innerHTML = `
          <div class="filter-table-wrapper">
            <table class="filter-table forumbox">
              <thead>
                <tr class="block_txt_c0">
                  <th class="c1" width="1">昵称</th>
                  <th class="c2">标记</th>
                  <th class="c3" width="1">过滤方式</th>
                  <th class="c4" width="1">操作</th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
        `;

        return element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = ({ id, name, tags, filterMode }, anchor = null) => {
        if (id === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

          anchor.className = `row${
            (tbody.querySelectorAll("TR").length % 2) + 1
          }`;

          tbody.appendChild(anchor);
        }

        anchor.innerHTML = `
          <td class="c1">
            ${format(id, name)}
          </td>
          <td class="c2">
            ${tags.map(tagModule.format).join("")}
          </td>
          <td class="c3">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c4">
            <div class="filter-table-button-group">
              <button>编辑</button>
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

        actions[0].onclick = () => {
          const filterMode = filterModule.switchModeByName(
            actions[0].innerHTML
          );

          actions[0].innerHTML = filterMode;

          edit(id, { filterMode });
          reFilter((item) => item.uid !== uid);
        };

        actions[1].onclick = () => {
          details(id, name, (item) => {
            load(item, anchor);
          });
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(id);
          reFilter((item) => item.uid !== uid);
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        details,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      format,
      view,
    };
  })();

  // 标记模块
  const tagModule = (() => {
    // 获取标记列表
    const list = () => dataModule.getTags();

    // 计算标记颜色
    // 采用的是泥潭的颜色方案,参见 commonui.htmlName
    const generateColor = (name) => {
      const hash = (() => {
        let h = 5381;

        for (var i = 0; i < name.length; i++) {
          h = ((h << 5) + h + name.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);
      });
    };

    // 获取标记
    const get = ({ id, name }) => {
      // 获取列表
      const tags = list();

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

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

        if (tag) {
          return tag;
        }
      }

      return null;
    };

    // 增加标记
    const add = (name) => {
      // 获取对应的标记
      const tag = get({ name });

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

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

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

      // 保存标记
      dataModule.save({
        tags: {
          [id]: {
            id,
            name,
            color,
            filterMode: filterModule.defaultMode,
          },
        },
      });

      // 返回标记信息
      return get({ id });
    };

    // 编辑标记
    const edit = (id, values) => {
      dataModule.save({
        tags: {
          [id]: values,
        },
      });
    };

    // 删除标记
    const remove = (id) => {
      // TODO 这里不可避免的直接操作了原始数据
      delete list()[id];

      // 删除用户对应的标记
      Object.values(userModule.list()).forEach((user) => {
        const index = user.tags.findIndex((tag) => tag === id);

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

      // 保存数据
      dataModule.save({});
    };

    // 格式化标记
    const format = (id, name, color) => {
      if (id) {
        const tag = get({ id });

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

      if (name && color) {
        return `<b class="block_txt nobr" style="background: ${color}; color: #FFF; margin: 0.1em 0.2em;">${name}</b>`;
      }

      return "";
    };

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.innerHTML = `
          <div class="filter-table-wrapper">
            <table class="filter-table forumbox">
              <thead>
                <tr class="block_txt_c0">
                  <th class="c1" width="1">标记</th>
                  <th class="c2">列表</th>
                  <th class="c3" width="1">过滤方式</th>
                  <th class="c4" width="1">操作</th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
        `;

        return element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = ({ id, filterMode }, anchor = null) => {
        if (id === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

          anchor.className = `row${
            (tbody.querySelectorAll("TR").length % 2) + 1
          }`;

          tbody.appendChild(anchor);
        }

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

        const filteredUsers = users.filter((user) => user.tags.includes(id));

        anchor.innerHTML = `
          <td class="c1">
            ${format(id)}
          </td>
          <td class="c2">
            <button>${filteredUsers.length}</button>
            <div style="white-space: normal; display: none;">
              ${filteredUsers
                .map((user) => userModule.format(user.id))
                .join("")}
            </div>
          </td>
          <td class="c3">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c4">
            <div class="filter-table-button-group">
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

        actions[0].onclick = (() => {
          let hide = true;

          return () => {
            hide = !hide;

            actions[0].nextElementSibling.style.display = hide
              ? "none"
              : "block";
          };
        })();

        actions[1].onclick = () => {
          const filterMode = filterModule.switchModeByName(
            actions[1].innerHTML
          );

          actions[1].innerHTML = filterMode;

          edit(id, { filterMode });
          reFilter((item) =>
            filteredUsers.find((user) => user.id === item.uid)
          );
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(id);
          reFilter((item) =>
            filteredUsers.find((user) => user.id === item.uid)
          );
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      format,
      generateColor,
      view,
    };
  })();

  // 关键字模块
  const keywordModule = (() => {
    // 获取关键字列表
    const list = () => dataModule.getKeywords();

    // 获取关键字
    const get = (id) => {
      // 获取列表
      const keywords = list();

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

      return null;
    };

    // 编辑关键字
    const edit = (id, values) => {
      dataModule.save({
        keywords: {
          [id]: values,
        },
      });
    };

    // 增加关键字
    // filterLevel: 0 - 仅过滤标题; 1 - 过滤标题和内容
    // 无需判重
    const add = (keyword, filterMode, filterLevel) => {
      // ID 为最大值 + 1
      const id = Math.max(Object.keys(list()), 0) + 1;

      // 保存关键字
      dataModule.save({
        keywords: {
          [id]: {
            id,
            keyword,
            filterMode,
            filterLevel,
          },
        },
      });

      // 返回关键字信息
      return get(id);
    };

    // 删除关键字
    const remove = (id) => {
      // TODO 这里不可避免的直接操作了原始数据
      delete list()[id];

      // 保存数据
      dataModule.save({});
    };

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.innerHTML = `
          <div class="filter-table-wrapper">
            <table class="filter-table forumbox">
              <thead>
                <tr class="block_txt_c0">
                  <th class="c1">列表</th>
                  <th class="c2" width="1">过滤方式</th>
                  <th class="c3" width="1">包括内容</th>
                  <th class="c4" width="1">操作</th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
          <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。</div>
        `;

        return element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = (
        { id, keyword, filterMode, filterLevel },
        anchor = null
      ) => {
        if (id === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

          anchor.className = `row${
            (tbody.querySelectorAll("TR").length % 2) + 1
          }`;

          tbody.appendChild(anchor);
        }

        const checked = filterLevel ? "checked" : "";

        anchor.innerHTML = `
          <td class="c1">
            <div class="filter-input-wrapper">
              <input type="text" value="${keyword || ""}" />
            </div>
          </td>
          <td class="c2">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c3">
            <div style="text-align: center;">
              <input type="checkbox" ${checked} />
            </div>
          </td>
          <td class="c4">
            <div class="filter-table-button-group">
              <button>保存</button>
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

        actions[0].onclick = () => {
          actions[0].innerHTML = filterModule.switchModeByName(
            actions[0].innerHTML
          );
        };

        actions[1].onclick = () => {
          const keyword = anchor.querySelector("INPUT[type='text']").value;

          const filterMode = actions[0].innerHTML;

          const filterLevel = anchor.querySelector(
            `INPUT[type="checkbox"]:checked`
          )
            ? 1
            : 0;

          if (keyword) {
            edit(id, {
              keyword,
              filterMode,
              filterLevel,
            });
            reFilter((item) => item.reason.indexOf("关键字") !== 0);
          }
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(id);
          reFilter((item) => item.reason.indexOf("关键字") !== 0);
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        if (hasNext === false) {
          const loadNew = () => {
            const row = document.createElement("TR");

            row.className = `row${
              (tbody.querySelectorAll("TR").length % 2) + 1
            }`;

            row.innerHTML = `
              <td class="c1">
                <div class="filter-input-wrapper">
                  <input type="text" value="" />
                </div>
              </td>
              <td class="c2">
                <div class="filter-table-button-group">
                  <button>${filterModule.defaultMode}</button>
                </div>
              </td>
              <td class="c3">
                <div style="text-align: center;">
                  <input type="checkbox" />
                </div>
              </td>
              <td class="c4">
                <div class="filter-table-button-group">
                  <button>添加</button>
                </div>
              </td>
            `;

            const actions = row.querySelectorAll("BUTTON");

            actions[0].onclick = () => {
              const filterMode = filterModule.switchModeByName(
                actions[0].innerHTML
              );

              actions[0].innerHTML = filterMode;
            };

            actions[1].onclick = () => {
              const keyword = row.querySelector("INPUT[type='text']").value;

              const filterMode = actions[0].innerHTML;

              const filterLevel = row.querySelector(
                `INPUT[type="checkbox"]:checked`
              )
                ? 1
                : 0;

              if (keyword) {
                const item = add(keyword, filterMode, filterLevel);

                load(item, row);
                loadNew();
                reFilter();
              }
            };

            tbody.appendChild(row);
          };

          loadNew();
        }

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      view,
    };
  })();

  // 属地模块
  const locationModule = (() => {
    // 获取属地列表
    const list = () => dataModule.getLocations();

    // 获取属地
    const get = (id) => {
      // 获取列表
      const locations = list();

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

      return null;
    };

    // 增加属地
    // 无需判重
    const add = (keyword, filterMode) => {
      // ID 为最大值 + 1
      const id = Math.max(Object.keys(list()), 0) + 1;

      // 保存属地
      dataModule.save({
        locations: {
          [id]: {
            id,
            keyword,
            filterMode,
          },
        },
      });

      // 返回属地信息
      return get(id);
    };

    // 编辑属地
    const edit = (id, values) => {
      dataModule.save({
        locations: {
          [id]: values,
        },
      });
    };

    // 删除属地
    const remove = (id) => {
      // TODO 这里不可避免的直接操作了原始数据
      delete list()[id];

      // 保存数据
      dataModule.save({});
    };

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.innerHTML = `
          <div class="filter-table-wrapper">
            <table class="filter-table forumbox">
              <thead>
                <tr class="block_txt_c0">
                  <th class="c1">列表</th>
                  <th class="c2" width="1">过滤方式</th>
                  <th class="c3" width="1">操作</th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
          <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。<br/>属地过滤功能需要占用额外的资源,请谨慎开启</div>
        `;

        return element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = ({ id, keyword, filterMode }, anchor = null) => {
        if (id === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

          anchor.className = `row${
            (tbody.querySelectorAll("TR").length % 2) + 1
          }`;

          tbody.appendChild(anchor);
        }

        anchor.innerHTML = `
          <td class="c1">
            <div class="filter-input-wrapper">
              <input type="text" value="${keyword || ""}" />
            </div>
          </td>
          <td class="c2">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c3">
            <div class="filter-table-button-group">
              <button>保存</button>
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

        actions[0].onclick = () => {
          actions[0].innerHTML = filterModule.switchModeByName(
            actions[0].innerHTML
          );
        };

        actions[1].onclick = () => {
          const keyword = anchor.querySelector("INPUT[type='text']").value;

          const filterMode = actions[0].innerHTML;

          if (keyword) {
            edit(id, {
              keyword,
              filterMode,
            });
            reFilter((item) => item.reason.indexOf("属地") !== 0);
          }
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(id);
          reFilter((item) => item.reason.indexOf("属地") !== 0);
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        if (hasNext === false) {
          const loadNew = () => {
            const row = document.createElement("TR");

            row.className = `row${
              (tbody.querySelectorAll("TR").length % 2) + 1
            }`;

            row.innerHTML = `
              <td class="c1">
                <div class="filter-input-wrapper">
                  <input type="text" value="" />
                </div>
              </td>
              <td class="c2">
                <div class="filter-table-button-group">
                  <button>${filterModule.defaultMode}</button>
                </div>
              </td>
              <td class="c3">
                <div class="filter-table-button-group">
                  <button>添加</button>
                </div>
              </td>
            `;

            const actions = row.querySelectorAll("BUTTON");

            actions[0].onclick = () => {
              const filterMode = filterModule.switchModeByName(
                actions[0].innerHTML
              );

              actions[0].innerHTML = filterMode;
            };

            actions[1].onclick = () => {
              const keyword = row.querySelector("INPUT[type='text']").value;

              const filterMode = actions[0].innerHTML;

              if (keyword) {
                const item = add(keyword, filterMode);

                load(item, row);
                loadNew();
                reFilter();
              }
            };

            tbody.appendChild(row);
          };

          loadNew();
        }

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      view,
    };
  })();

  // 猎巫模块
  const witchHuntModule = (() => {
    const key = "WITCH_HUNT";

    const queue = [];

    const cache = {};

    // 获取设置列表
    const list = () => GM_getValue(key) || {};

    // 获取单条设置
    const get = (fid) => {
      // 获取列表
      const settings = list();

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

      return null;
    };

    // 增加设置
    // filterLevel: 0 - 仅标记; 1 - 标记并过滤
    const add = async (fid, label, filterMode, filterLevel) => {
      // FID 只能是数字
      fid = parseInt(fid, 10);

      // 获取列表
      const settings = list();

      // 如果版面 ID 已存在,则提示错误
      if (Object.keys(settings).includes(fid)) {
        alert("已有相同版面ID");
        return;
      }

      // 请求版面信息
      const info = await fetchModule.getForumInfo(fid);

      // 如果版面不存在,则提示错误
      if (info === null) {
        alert("版面ID有误");
        return;
      }

      // 计算标记颜色
      const color = tagModule.generateColor(info.name);

      // 保存设置
      settings[fid] = {
        fid,
        name: info.name,
        label,
        color,
        filterMode,
        filterLevel,
      };

      GM_setValue(key, settings);

      // 增加后需要清除缓存
      Object.keys(cache).forEach((key) => {
        delete cache[key];
      });

      // 返回设置信息
      return settings[fid];
    };

    // 编辑设置
    const edit = (fid, values) => {
      // 获取列表
      const settings = list();

      // 没有则跳过
      if (settings[fid] === undefined) {
        return;
      }

      // 保存设置
      settings[fid] = {
        ...settings[fid],
        ...values,
      };

      GM_setValue(key, settings);

      // 编辑后需要重新加载
      reFilter((item) => {
        item.witchHunt = null;

        return true;
      });
    };

    // 删除设置
    const remove = (fid) => {
      // 获取列表
      const settings = list();

      // 没有则跳过
      if (settings[fid] === undefined) {
        return;
      }

      // 保存设置
      delete settings[fid];

      GM_setValue(key, settings);

      // 删除后需要清除缓存
      Object.keys(cache).forEach((key) => {
        delete cache[key];
      });
    };

    // 格式化版面
    const format = (fid, name) => {
      return `<a href="/thread.php?fid=${fid}" class="b nobr">[${name}]</a>`;
    };

    // 猎巫
    const run = (item) => {
      item.witchHunt = item.witchHunt || [];

      // 重新过滤
      const reload = (newValue) => {
        const isEqual = newValue.sort().join() === item.witchHunt.sort().join();

        if (isEqual) {
          return;
        }

        item.witchHunt = newValue;
        item.execute();
      };

      // 获取列表
      const settings = Object.keys(list());

      // 没有设置且没有旧数据,直接跳过
      if (settings.length === 0 && item.witchHunt.length === 0) {
        return;
      }

      // 猎巫任务
      const task = async () => {
        // 请求版面发言记录
        const result = cache[item.uid]
          ? cache[item.uid]
          : (
              await Promise.all(
                settings.map(async (fid) => {
                  // 当前版面发言记录
                  const result = await fetchModule.getForumPosted(
                    fid,
                    item.uid
                  );

                  // 写入当前设置
                  if (result) {
                    return parseInt(fid, 10);
                  }

                  return null;
                })
              )
            ).filter((i) => i !== null);

        // 写入缓存,同一个页面多次请求没意义
        cache[item.uid] = result;

        // 执行完毕,如果结果有变,重新过滤
        reload(result);

        // 将当前任务移出队列
        queue.shift();

        // 如果还有任务,继续执行
        if (queue.length > 0) {
          queue[0]();
        }
      };

      // 队列里已经有任务
      const isRunning = queue.length > 0;

      // 加入队列
      queue.push(task);

      // 如果没有正在执行的任务,则立即执行
      if (isRunning === false) {
        task();
      }
    };

    // 重置
    const clear = () => {
      reFilter((item) => {
        run(item);

        return true;
      });
    };

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.innerHTML = `
          <div class="filter-table-wrapper">
            <table class="filter-table forumbox">
              <thead>
                <tr class="block_txt_c0">
                  <th class="c1" width="1">版面</th>
                  <th class="c2">标签</th>
                  <th class="c3" width="1">启用过滤</th>
                  <th class="c4" width="1">过滤方式</th>
                  <th class="c5" width="1">操作</th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
          <div class="silver" style="margin-top: 10px;">猎巫模块需要占用额外的资源,请谨慎开启</div>
        `;

        return element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = (
        { fid, name, label, color, filterMode, filterLevel },
        anchor = null
      ) => {
        if (fid === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

          anchor.className = `row${
            (tbody.querySelectorAll("TR").length % 2) + 1
          }`;

          tbody.appendChild(anchor);
        }

        const checked = filterLevel ? "checked" : "";

        anchor.innerHTML = `
          <td class="c1">
            ${format(fid, name)}
          </td>
          <td class="c2">
            ${tagModule.format(null, label, color)}
          </td>
          <td class="c3">
            <div style="text-align: center;">
              <input type="checkbox" ${checked} />
            </div>
          </td>
          <td class="c4">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c5">
            <div class="filter-table-button-group">
              <button>保存</button>
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

        actions[0].onclick = () => {
          actions[0].innerHTML = filterModule.switchModeByName(
            actions[0].innerHTML
          );
        };

        actions[1].onclick = () => {
          const filterMode = actions[0].innerHTML;

          const filterLevel = anchor.querySelector(
            `INPUT[type="checkbox"]:checked`
          )
            ? 1
            : 0;

          edit(fid, {
            filterMode,
            filterLevel,
          });
          clear();
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(fid);
          clear();
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        if (hasNext === false) {
          const loadNew = () => {
            const row = document.createElement("TR");

            row.className = `row${
              (tbody.querySelectorAll("TR").length % 2) + 1
            }`;

            row.innerHTML = `
              <td class="c1" style="min-width: 200px;">
                <div class="filter-input-wrapper">
                  <input type="text" value="" placeholder="版面ID" />
                </div>
              </td>
              <td class="c2">
                <div class="filter-input-wrapper">
                  <input type="text" value="" placeholder="标签" />
                </div>
              </td>
              <td class="c3">
                <div style="text-align: center;">
                  <input type="checkbox" />
                </div>
              </td>
              <td class="c4">
                <div class="filter-table-button-group">
                  <button>${filterModule.defaultMode}</button>
                </div>
              </td>
              <td class="c5">
                <div class="filter-table-button-group">
                  <button>添加</button>
                </div>
              </td>
            `;

            const actions = row.querySelectorAll("BUTTON");

            actions[0].onclick = () => {
              const filterMode = filterModule.switchModeByName(
                actions[0].innerHTML
              );

              actions[0].innerHTML = filterMode;
            };

            actions[1].onclick = async () => {
              const inputs = row.querySelectorAll("INPUT[type='text']");

              const fid = inputs[0].value;
              const label = inputs[1].value;

              const filterMode = actions[0].innerHTML;

              const filterLevel = row.querySelector(
                `INPUT[type="checkbox"]:checked`
              )
                ? 1
                : 0;

              if (fid && label) {
                const item = await add(fid, label, filterMode, filterLevel);

                if (item) {
                  load(item, row);
                  loadNew();
                  clear();
                }
              }
            };

            tbody.appendChild(row);
          };

          loadNew();
        }

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      run,
      view,
    };
  })();

  // 通用设置
  const commonModule = (() => {
    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";

        return element;
      })();

      const refresh = () => {
        content.innerHTML = "";

        // 前置过滤
        (() => {
          const checked = preFilter ? "checked" : "";

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

          element.innerHTML += `
            <div>
              <label>
                前置过滤
                <input type="checkbox" ${checked} />
              </label>
            </div>
          `;

          const checkbox = element.querySelector("INPUT");

          checkbox.onchange = () => {
            const newValue = checkbox.checked;

            GM_setValue(PRE_FILTER_KEY, newValue);

            location.reload();
          };

          content.appendChild(element);
        })();

        // 默认过滤方式
        (() => {
          const element = document.createElement("DIV");

          element.innerHTML += `
            <br/>
            <div>默认过滤方式</div>
            <div></div>
            <div class="silver" style="margin-top: 10px;">${filterModule.tips}</div>
          `;

          ["标记", "遮罩", "隐藏"].forEach((item, index) => {
            const span = document.createElement("SPAN");

            const checked =
              dataModule.getDefaultFilterMode() === item ? "checked" : "";

            span.innerHTML += `
              <input id="s-fm-${index}" type="radio" name="filterType" ${checked}>
              <label for="s-fm-${index}" style="cursor: pointer;">${item}</label>
            `;

            const input = span.querySelector("INPUT");

            input.onchange = () => {
              if (input.checked) {
                dataModule.setDefaultFilterMode(item);

                reFilter((item) => item.filterMode === "继承");
              }
            };

            element.querySelectorAll("div")[1].append(span);
          });

          content.appendChild(element);
        })();

        // 小号过滤(时间)
        (() => {
          const value = dataModule.getFilterRegdateLimit() / 86400000;

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

          element.innerHTML += `
            <br/>
            <div>
              隐藏注册时间小于<input value="${value}" maxLength="4" style="width: 48px;" />天的用户
              <button>确认</button>
            </div>
          `;

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            const newValue =
              parseInt(element.querySelector("INPUT").value, 10) || 0;

            dataModule.setFilterRegdateLimit(
              newValue < 0 ? 0 : newValue * 86400000
            );

            reFilter((item) => item.reason.indexOf("注册时间") !== 0);
          };

          content.appendChild(element);
        })();

        // 小号过滤(发帖数)
        (() => {
          const value = dataModule.getFilterPostnumLimit();

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

          element.innerHTML += `
            <br/>
            <div>
              隐藏发帖数量小于<input value="${value}" maxLength="5" style="width: 48px;" />贴的用户
              <button>确认</button>
            </div>
          `;

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            const newValue =
              parseInt(element.querySelector("INPUT").value, 10) || 0;

            dataModule.setFilterPostnumLimit(newValue < 0 ? 0 : newValue);

            reFilter((item) => item.reason.indexOf("发帖数量") !== 0);
          };

          content.appendChild(element);
        })();

        // 流量号过滤(主题比例)
        (() => {
          const value = dataModule.getFilterTopicRateLimit();

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

          element.innerHTML += `
            <br/>
            <div>
              隐藏发帖比例大于<input value="${value}" maxLength="3" style="width: 48px;" />%的用户
              <button>确认</button>
            </div>
          `;

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            const newValue =
              parseInt(element.querySelector("INPUT").value, 10) || 100;

            if (newValue <= 0 || newValue > 100) {
              return;
            }

            dataModule.setFilterTopicRateLimit(newValue);

            reFilter((item) => item.reason.indexOf("发帖比例") !== 0);
          };

          content.appendChild(element);
        })();

        // 声望过滤
        (() => {
          const value = dataModule.getFilterReputationLimit() || "";

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

          element.innerHTML += `
            <br/>
            <div>
              隐藏版面声望低于<input value="${value}" maxLength="5" style="width: 48px;" />点的用户
              <button>确认</button>
            </div>
          `;

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            const newValue = parseInt(element.querySelector("INPUT").value, 10);

            dataModule.setFilterReputationLimit(newValue);

            reFilter((item) => item.reason.indexOf("版面声望") !== 0);
          };

          content.appendChild(element);
        })();

        // 匿名过滤
        (() => {
          const checked = dataModule.getFilterAnony() ? "checked" : "";

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

          element.innerHTML += `
            <br/>
            <div>
              <label>
                隐藏匿名的用户
                <input type="checkbox" ${checked} />
              </label>
            </div>
          `;

          const checkbox = element.querySelector("INPUT");

          checkbox.onchange = () => {
            const newValue = checkbox.checked;

            dataModule.setFilterAnony(newValue);

            reFilter((item) => item.reason.indexOf("匿名") !== 0);
          };

          content.appendChild(element);
        })();

        // 删除没有标记的用户
        (() => {
          const element = document.createElement("DIV");

          element.innerHTML += `
            <br/>
            <div>
              <button>删除没有标记的用户</button>
            </div>
          `;

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

            const filteredUsers = Object.values(userModule.list()).filter(
              ({ tags }) => tags.length === 0
            );

            filteredUsers.forEach(({ id }) => {
              userModule.remove(id);
            });

            reFilter((item) =>
              filteredUsers.find((user) => user.id === item.uid)
            );
          };

          content.appendChild(element);
        })();

        // 删除没有用户的标记
        (() => {
          const element = document.createElement("DIV");

          element.innerHTML += `
            <br/>
            <div>
              <button>删除没有用户的标记</button>
            </div>
          `;

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

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

            Object.values(tagModule.list()).forEach(({ id }) => {
              if (users.find(({ tags }) => tags.includes(id))) {
                return;
              }

              tagModule.remove(id);
            });
          };

          content.appendChild(element);
        })();

        // 删除非激活中的用户
        (() => {
          const element = document.createElement("DIV");

          element.innerHTML += `
            <br/>
            <div>
              <button>删除非激活中的用户</button>
              <div style="white-space: normal;"></div>
            </div>
          `;

          const action = element.querySelector("BUTTON");
          const list = action.nextElementSibling;

          action.onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

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

            const filtered = [];

            const waitingQueue = users.map(
              ({ id }) =>
                () =>
                  fetchModule.getUserInfo(id).then(({ bit }) => {
                    const activeInfo = commonui.activeInfo(0, 0, bit);

                    const activeType = activeInfo[1];

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

                    list.innerHTML += userModule.format(id);

                    filtered.push(id);

                    userModule.remove(id);
                  })
            );

            const queueLength = waitingQueue.length;

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

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

                action.disabled = true;

                next().finally(execute);
              } else {
                action.disabled = false;

                reFilter((item) => filtered.includes(item.uid));
              }
            };

            execute();
          };

          content.appendChild(element);
        })();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      view,
    };
  })();

  // 额外数据请求模块
  // 临时的缓存写法
  const fetchModule = (() => {
    // 简单的统一请求
    const request = (url, config = {}) =>
      fetch(url, {
        headers: {
          "X-User-Agent": USER_AGENT,
        },
        ...config,
      });

    // 获取主题数量
    // 缓存 1 小时
    const getTopicNum = (() => {
      const name = "TOPIC_NUM_CACHE";

      const expireTime = 60 * 60 * 1000;

      cacheModule.init(name, {
        keyPath: "uid",
        version: 1,
      });

      return async (uid) => {
        const cache = await cacheModule.load(name, uid, expireTime);

        if (cache) {
          return cache.count;
        }

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

        const { __ROWS } = await new Promise((resolve) => {
          request(api)
            .then((res) => res.blob())
            .then((blob) => {
              const reader = new FileReader();

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

                  resolve(result.data);
                } catch {
                  resolve({});
                }
              };

              reader.readAsText(blob, "GBK");
            })
            .catch(() => {
              resolve({});
            });
        });

        cacheModule.save(name, uid, {
          uid,
          count: __ROWS,
          timestamp: new Date().getTime(),
        });

        return __ROWS;
      };
    })();

    // 获取用户信息
    // 缓存 1 小时
    const getUserInfo = (() => {
      const name = "USER_INFO_CACHE";

      const expireTime = 60 * 60 * 1000;

      cacheModule.init(name, {
        keyPath: "uid",
        version: 1,
      });

      return async (uid) => {
        const cache = await cacheModule.load(name, uid, expireTime);

        if (cache) {
          return cache.data;
        }

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

        const data = await new Promise((resolve) => {
          request(api)
            .then((res) => res.blob())
            .then((blob) => {
              const reader = new FileReader();

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

                  resolve(result.data[0] || null);
                } catch {
                  resolve(null);
                }
              };

              reader.readAsText(blob, "GBK");
            })
            .catch(() => {
              resolve(null);
            });
        });

        if (data) {
          cacheModule.save(name, uid, {
            uid,
            data,
            timestamp: new Date().getTime(),
          });
        }

        return data;
      };
    })();

    // 获取顶楼用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、声望
    // 缓存 10 分钟
    const getUserInfoAndReputation = (() => {
      const name = "PAGE_CACHE";

      const expireTime = 10 * 60 * 1000;

      cacheModule.init(name, {
        keyPath: "url",
        version: 1,
      });

      return async (tid, pid) => {
        if (tid === undefined && pid === undefined) {
          return;
        }

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

        const cache = await cacheModule.load(name, api, expireTime);

        if (cache) {
          return cache.data;
        }

        // 请求数据
        const data = await new Promise((resolve) => {
          request(api)
            .then((res) => res.blob())
            .then((blob) => {
              const getLastIndex = (content, position) => {
                if (position >= 0) {
                  let nextIndex = position + 1;

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

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

                      if (nextIndex < 0) {
                        break;
                      }
                    }

                    nextIndex = nextIndex + 1;
                  }
                }

                return -1;
              };

              const reader = new FileReader();

              reader.onload = async () => {
                const parser = new DOMParser();

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

                const html = doc.body.innerHTML;

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

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

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

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

                    return 0;
                  })();

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

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

                  // 非匿名用户
                  if (uid && uid > 0) {
                    // 取得用户信息
                    const userInfo = (() => {
                      // 起始JSON
                      const str = `"${uid}":{`;

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

                      // 结尾下标
                      const lastIndex = getLastIndex(html, index);

                      if (lastIndex >= 0) {
                        try {
                          return JSON.parse(
                            `{${html.substring(index, lastIndex)}}`
                          );
                        } catch {}
                      }

                      return null;
                    })();

                    // 取得用户声望
                    const reputation = (() => {
                      const reputations = (() => {
                        // 起始JSON
                        const str = `"__REPUTATIONS":{`;

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

                        // 结尾下标
                        const lastIndex = getLastIndex(html, index);

                        if (lastIndex >= 0) {
                          return JSON.parse(
                            `{${html.substring(index, lastIndex)}}`
                          );
                        }

                        return null;
                      })();

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

                      return NaN;
                    })();

                    resolve({
                      uid,
                      subject,
                      content,
                      userInfo,
                      reputation,
                    });
                    return;
                  }

                  resolve({
                    uid,
                    subject,
                    content,
                  });
                  return;
                }

                resolve(null);
              };

              reader.readAsText(blob, "GBK");
            })
            .catch(() => {
              resolve(null);
            });
        });

        if (data) {
          cacheModule.save(name, api, {
            url: api,
            data,
            timestamp: new Date().getTime(),
          });
        }

        return data;
      };
    })();

    // 获取版面信息
    // 不会频繁调用,无需缓存
    const getForumInfo = async (fid) => {
      if (Number.isNaN(fid)) {
        return null;
      }

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

      const data = await new Promise((resolve) => {
        request(api)
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

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

                if (result.data) {
                  resolve(result.data.__F || null);
                  return;
                }

                resolve(null);
              } catch {
                resolve(null);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve(null);
          });
      });

      return data;
    };

    // 获取版面发言记录
    // 缓存 1 天
    const getForumPosted = (() => {
      const name = "FORUM_POSTED_CACHE";

      const expireTime = 24 * 60 * 60 * 1000;

      cacheModule.init(name, {
        keyPath: "url",
        persistent: true,
        version: 2,
      });

      return async (fid, uid) => {
        if (uid <= 0) {
          return;
        }

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

        const cache = await cacheModule.load(name, api);

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

          return cache.data;
        }

        let isComplete = false;
        let isBusy = false;

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

            request(url)
              .then((res) => res.blob())
              .then((blob) => {
                const reader = new FileReader();

                reader.onload = () => {
                  const text = reader.result;

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

                  if (matched) {
                    [
                      ...new Set(
                        matched.map((item) =>
                          parseInt(item.match(/-?\d+/)[0], 10)
                        )
                      ),
                    ].forEach((item) => {
                      const key = api.replace(`&fid=${fid}`, `&fid=${item}`);

                      // 直接写入缓存
                      cacheModule.save(name, key, {
                        url: key,
                        data: true,
                        timestamp: new Date().getTime(),
                      });

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

                    resolve();
                    return;
                  }

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

                  resolve();
                };

                reader.readAsText(blob, "GBK");
              })
              .catch(() => {
                resolve();
              });
          });

        // 先获取回复记录的第一页,顺便可以获取其他版面的记录
        // 没有再通过版面接口获取,避免频繁出现“服务器忙,请稍后重试”的错误
        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 - 60 * 60 * 1000)
            : new Date().getTime();

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

        return isComplete;
      };
    })();

    // 每天清理缓存
    (() => {
      const today = new Date();

      const lastTime = new Date(GM_getValue(CLEAR_TIME_KEY) || 0);

      const isToday =
        lastTime.getDate() === today.getDate() &&
        lastTime.getMonth() === today.getMonth() &&
        lastTime.getFullYear() === today.getFullYear();

      if (isToday === false) {
        cacheModule.clear();

        GM_setValue(CLEAR_TIME_KEY, today.getTime());
      }
    })();

    return {
      getTopicNum,
      getUserInfo,
      getUserInfoAndReputation,
      getForumInfo,
      getForumPosted,
    };
  })();

  // UI
  const ui = (() => {
    const modules = {};

    // 主界面
    const view = (() => {
      const tabContainer = (() => {
        const element = document.createElement("DIV");

        element.className = "w100";
        element.innerHTML = `
          <div class="right_" style="margin-bottom: 5px;">
            <table class="stdbtn" cellspacing="0">
              <tbody>
                <tr></tr>
              </tbody>
            </table>
          </div>
          <div class="clear"></div>
          `;

        return element;
      })();

      const tabPanelContainer = (() => {
        const element = document.createElement("DIV");

        element.style = "width: 80vw;";

        return element;
      })();

      const content = (() => {
        const element = document.createElement("DIV");

        element.appendChild(tabContainer);
        element.appendChild(tabPanelContainer);

        return element;
      })();

      const addModule = (() => {
        const tc = tabContainer.querySelector("TR");
        const cc = tabPanelContainer;

        return (name, module) => {
          const tabBox = document.createElement("TD");

          tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${name}</a>`;

          const tab = tabBox.childNodes[0];

          const toggle = () => {
            Object.values(modules).forEach((item) => {
              if (item.tab === tab) {
                item.tab.className = "nobr";
                item.content.style = "display: block";
                item.refresh();
              } else {
                item.tab.className = "nobr silver";
                item.content.style = "display: none";
              }
            });
          };

          tc.append(tabBox);
          cc.append(module.content);

          tab.onclick = toggle;

          modules[name] = {
            ...module,
            tab,
            toggle,
          };

          return modules[name];
        };
      })();

      return {
        content,
        addModule,
      };
    })();

    // 右上角菜单
    const menu = (() => {
      const container = document.createElement("DIV");

      container.className = `td`;
      container.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">屏蔽</a>`;

      const content = container.querySelector("A");

      const create = (onclick) => {
        const anchor = document.querySelector("#mainmenu .td:last-child");

        if (anchor) {
          anchor.before(container);

          content.onclick = onclick;

          return true;
        }

        return false;
      };

      const update = (list) => {
        const count = list.length;

        if (count) {
          content.innerHTML = `屏蔽 <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
        } else {
          content.innerHTML = `屏蔽`;
        }
      };

      return {
        create,
        update,
      };
    })();

    return {
      ...view,
      ...menu,
    };
  })();

  // 判断是否为当前用户 UID
  const isCurrentUID = (uid) => {
    return unsafeWindow.__CURRENT_UID === parseInt(uid, 10);
  };

  // 获取过滤方式
  const getFilterMode = async (item) => {
    // 声明结果
    const result = {
      mode: -1,
      reason: ``,
    };

    // 获取 UID
    const uid = parseInt(item.uid, 10);

    // 获取链接参数
    const params = new URLSearchParams(location.search);

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

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

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

    // 跳过自己
    if (isCurrentUID(uid)) {
      return "";
    }

    // 用户过滤
    (() => {
      // 获取屏蔽列表里匹配的用户
      const user = userModule.get(uid);

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

      const { filterMode } = user;

      const mode = filterModule.getModeByName(filterMode);

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

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

    // 标记过滤
    (() => {
      // 获取屏蔽列表里匹配的用户
      const user = userModule.get(uid);

      // 获取用户对应的标记,并跳过低于当前的过滤模式
      const tags = user
        ? user.tags
            .map((id) => tagModule.get({ id }))
            .filter((i) => i !== null)
            .filter(
              (i) => filterModule.getModeByName(i.filterMode) > result.mode
            )
        : [];

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

      // 取最高的过滤模式
      const { filterMode, name } = tags.sort(
        (a, b) =>
          filterModule.getModeByName(b.filterMode) -
          filterModule.getModeByName(a.filterMode)
      )[0];

      const mode = filterModule.getModeByName(filterMode);

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

    // 关键字过滤
    await (async () => {
      const { getContent } = item;

      // 获取设置里的关键字列表,并跳过低于当前的过滤模式
      const keywords = Object.values(keywordModule.list()).filter(
        (i) => filterModule.getModeByName(i.filterMode) > result.mode
      );

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

      // 根据过滤等级依次判断
      const list = keywords.sort(
        (a, b) =>
          filterModule.getModeByName(b.filterMode) -
          filterModule.getModeByName(a.filterMode)
      );

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

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

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

          const match = subject.match(keyword);

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

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

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

            return item.content || null;
          })();

          if (content) {
            const match = content.match(keyword);

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

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

    // 杂项过滤
    // 放在属地前是因为符合条件的过多,没必要再请求它们的属地
    await (async () => {
      const { getUserInfo, getReputation } = item;

      // 如果当前模式是显示,则跳过
      if (filterModule.getNameByMode(result.mode) === "显示") {
        return;
      }

      // 获取隐藏模式下标
      const mode = filterModule.getModeByName("隐藏");

      // 匿名
      if (uid <= 0) {
        const filterAnony = dataModule.getFilterAnony();

        if (filterAnony) {
          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = "匿名";
        }

        return;
      }

      // 注册时间过滤
      await (async () => {
        const filterRegdateLimit = dataModule.getFilterRegdateLimit();

        // 如果没有用户信息,则请求
        const userInfo = await (async () => {
          if (item.userInfo === undefined) {
            await getUserInfo().catch(() => {});
          }

          return item.userInfo || {};
        })();

        const { regdate } = userInfo;

        if (regdate === undefined) {
          return;
        }

        if (
          filterRegdateLimit > 0 &&
          regdate * 1000 > new Date() - filterRegdateLimit
        ) {
          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `注册时间: ${new Date(
            regdate * 1000
          ).toLocaleDateString()}`;
          return;
        }
      })();

      // 发帖数量过滤
      await (async () => {
        const filterPostnumLimit = dataModule.getFilterPostnumLimit();

        // 如果没有用户信息,则请求
        const userInfo = await (async () => {
          if (item.userInfo === undefined) {
            await getUserInfo().catch(() => {});
          }

          return item.userInfo || {};
        })();

        const { postnum } = userInfo;

        if (postnum === undefined) {
          return;
        }

        if (filterPostnumLimit > 0 && postnum < filterPostnumLimit) {
          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `发帖数量: ${postnum}`;
          return;
        }
      })();

      // 发帖比例过滤
      await (async () => {
        const filterTopicRateLimit = dataModule.getFilterTopicRateLimit();

        // 如果没有用户信息,则请求
        const userInfo = await (async () => {
          if (item.userInfo === undefined) {
            await getUserInfo().catch(() => {});
          }

          return item.userInfo || {};
        })();

        const { postnum } = userInfo;

        if (postnum === undefined) {
          return;
        }

        if (filterTopicRateLimit > 0 && filterTopicRateLimit < 100) {
          // 获取主题数量
          const topicNum = await fetchModule.getTopicNum(uid);

          // 计算发帖比例
          const topicRate = (topicNum / postnum) * 100;

          if (topicRate > filterTopicRateLimit) {
            // 更新过滤模式和原因
            result.mode = mode;
            result.reason = `发帖比例: ${topicRate.toFixed(
              0
            )}% (${topicNum}/${postnum})`;
            return;
          }
        }
      })();

      // 版面声望过滤
      await (async () => {
        const filterReputationLimit = dataModule.getFilterReputationLimit();

        if (Number.isNaN(filterReputationLimit)) {
          return;
        }

        // 如果没有版面声望,则请求
        const reputation = await (async () => {
          if (item.reputation === undefined) {
            await getReputation().catch(() => {});
          }

          return item.reputation || NaN;
        })();

        if (reputation < filterReputationLimit) {
          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `版面声望: ${reputation}`;
          return;
        }
      })();
    })();

    // 属地过滤
    await (async () => {
      // 匿名用户则跳过
      if (uid <= 0) {
        return;
      }

      // 获取设置里的属地列表,并跳过低于当前的过滤模式
      const locations = Object.values(locationModule.list()).filter(
        (i) => filterModule.getModeByName(i.filterMode) > result.mode
      );

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

      // 请求属地
      const { ipLoc } = await fetchModule.getUserInfo(uid);

      // 请求失败则跳过
      if (ipLoc === undefined) {
        return;
      }

      // 根据过滤等级依次判断
      const list = locations.sort(
        (a, b) =>
          filterModule.getModeByName(b.filterMode) -
          filterModule.getModeByName(a.filterMode)
      );

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

        const match = ipLoc.match(keyword);

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

          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `属地: ${ipLoc}`;
          return;
        }
      }
    })();

    // 猎巫过滤
    (() => {
      // 获取猎巫结果
      const witchHunt = item.witchHunt;

      // 没有则跳过
      if (witchHunt === undefined) {
        return;
      }

      // 获取设置
      const list = Object.values(witchHuntModule.list()).filter(({ fid }) =>
        witchHunt.includes(fid)
      );

      // 筛选出匹配的猎巫
      const filtered = Object.values(list)
        .filter(({ filterLevel }) => filterLevel > 0)
        .filter(
          ({ filterMode }) =>
            filterModule.getModeByName(filterMode) > result.mode
        );

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

      // 取最高的过滤模式
      const { filterMode, label } = filtered.sort(
        (a, b) =>
          filterModule.getModeByName(b.filterMode) -
          filterModule.getModeByName(a.filterMode)
      )[0];

      const mode = filterModule.getModeByName(filterMode);

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `猎巫: ${label}`;
    })();

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

    // 写入列表
    listModule.add(item);

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

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

  // 获取主题过滤方式
  const getFilterModeByTopic = async (topic) => {
    const { tid } = topic;

    // 绑定额外的数据请求方式
    if (topic.getContent === undefined) {
      // 获取帖子内容,按需调用
      const getTopic = () =>
        new Promise((resolve, reject) => {
          // 避免重复请求
          if (topic.content || topic.userInfo || topic.reputation) {
            resolve(topic);
            return;
          }

          // 请求并写入数据
          fetchModule
            .getUserInfoAndReputation(tid, undefined)
            .then(({ subject, content, userInfo, reputation }) => {
              // 写入用户名
              if (userInfo) {
                topic.username = userInfo.username;
              }

              // 写入用户信息和声望
              topic.userInfo = userInfo;
              topic.reputation = reputation;

              // 写入帖子标题和内容
              topic.subject = subject;
              topic.content = content;

              // 返回结果
              resolve(topic);
            })
            .catch(reject);
        });

      // 绑定请求方式
      topic.getContent = getTopic;
      topic.getUserInfo = getTopic;
      topic.getReputation = getTopic;
    }

    // 获取过滤模式
    const filterMode = await getFilterMode(topic);

    // 返回结果
    return filterMode;
  };

  // 获取回复过滤方式
  const getFilterModeByReply = async (reply) => {
    const { tid, pid, uid } = reply;

    // 回复页面可以直接获取到用户信息和声望
    if (uid > 0) {
      // 取得用户信息
      const userInfo = commonui.userInfo.users[uid];

      // 取得用户声望
      const reputation = (() => {
        const reputations = commonui.userInfo.reputations;

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

        return NaN;
      })();

      // 写入用户名
      if (userInfo) {
        reply.username = userInfo.username;
      }

      // 写入用户信息和声望
      reply.userInfo = userInfo;
      reply.reputation = reputation;
    }

    // 绑定额外的数据请求方式
    if (reply.getContent === undefined) {
      // 获取帖子内容,按需调用
      const getReply = () =>
        new Promise((resolve, reject) => {
          // 避免重复请求
          if (reply.userInfo || reply.reputation) {
            resolve(reply);
            return;
          }

          // 请求并写入数据
          fetchModule
            .getUserInfoAndReputation(tid, pid)
            .then(({ subject, content, userInfo, reputation }) => {
              // 写入用户名
              if (userInfo) {
                reply.username = userInfo.username;
              }

              // 写入用户信息和声望
              reply.userInfo = userInfo;
              reply.reputation = reputation;

              // 写入帖子标题和内容
              reply.subject = subject;
              reply.content = content;

              // 返回结果
              resolve(reply);
            })
            .catch(reject);
        });

      // 绑定请求方式
      reply.getContent = getReply;
      reply.getUserInfo = getReply;
      reply.getReputation = getReply;
    }

    // 获取过滤模式
    const filterMode = await getFilterMode(reply);

    // 返回结果
    return filterMode;
  };

  // 处理引用
  const handleQuote = async (content) => {
    const quotes = content.querySelectorAll(".quote");

    await Promise.all(
      [...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 res[1];
            }
          }

          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 {};
        })();

        // 获取过滤方式
        const filterMode = await getFilterModeByReply({
          uid,
          tid,
          pid,
          subject: "",
          content: quote.innerText,
        });

        (() => {
          if (filterMode === "标记") {
            filterModule.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;
          }
        })();
      })
    );
  };

  // 过滤主题
  const filterTopic = async (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 container = title.closest("tr");

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

        // 样式处理
        (() => {
          // 还原样式
          // TODO 应该整体采用 className 来实现
          (() => {
            // 标记模式
            container.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");
          }
        })();

        // 猎巫会影响效率,待猎巫结果出来后再次过滤
        witchHuntModule.run(item.nFilter);
      };

      // 绑定事件
      item.nFilter = {
        tid,
        uid,
        username,
        container,
        title,
        author,
        subject,
        execute,
      };
    }

    // 等待过滤完成
    await item.nFilter.execute();
  };

  // 过滤回复
  const filterReply = async (item) => {
    // 绑定事件
    if (item.nFilter === undefined) {
      // 回复 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 getFilterModeByReply(item.nFilter);

        // 样式处理
        await (async () => {
          // 还原样式
          // 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";
              }

              filterModule.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;
            }
          })();

          // 处理引用
          await handleQuote(content);

          // 非隐藏模式下,恢复显示
          // 如果是隐藏模式,没必要再加载按钮和标记
          if (filterMode !== "隐藏") {
            // 获取当前用户
            const user = userModule.get(uid);

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

            // 加载标记和猎巫
            if (tags) {
              const witchHunt = item.nFilter.witchHunt || [];

              const list = [
                ...(user
                  ? user.tags
                      .map((id) => tagModule.get({ id }))
                      .map((tag) => tagModule.format(tag.id)) || []
                  : []),
                ...Object.values(witchHuntModule.list())
                  .filter(({ fid }) => witchHunt.includes(fid))
                  .map(({ label, color }) =>
                    tagModule.format(null, label, color)
                  ),
              ];

              tags.style.display = list.length ? "" : "none";
              tags.innerHTML = list.join("");
            }

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

        // 猎巫会影响效率,待猎巫结果出来后再次过滤
        witchHuntModule.run(item.nFilter);
      };

      // 绑定操作按钮事件
      (() => {
        if (action) {
          // 隐藏匿名操作按钮
          if (uid <= 0) {
            action.style.display = "none";
            return;
          }

          action.innerHTML = `屏蔽`;
          action.onclick = (e) => {
            const user = userModule.get(uid);

            if (e.ctrlKey === false) {
              userModule.view.details(uid, username, execute);
              return;
            }

            if (user) {
              userModule.remove(uid);
            } else {
              userModule.add(uid, username, [], filterModule.defaultMode);
            }

            execute();
          };
        }
      })();

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

    // 等待过滤完成
    await item.nFilter.execute();
  };

  // 加载 UI
  const loadUI = () => {
    // 右上角菜单
    const result = (() => {
      let window;

      return ui.create(() => {
        if (window === undefined) {
          window = commonui.createCommmonWindow();
        }

        window._.addContent(null);
        window._.addTitle(`屏蔽`);
        window._.addContent(ui.content);
        window._.show();
      });
    })();

    // 加载失败
    if (result === false) {
      return;
    }

    // 模块
    ui.addModule("列表", listModule.view).toggle();
    ui.addModule("用户", userModule.view);
    ui.addModule("标记", tagModule.view);
    ui.addModule("关键字", keywordModule.view);
    ui.addModule("属地", locationModule.view);
    ui.addModule("猎巫", witchHuntModule.view);
    ui.addModule("通用设置", commonModule.view);

    // 绑定列表更新回调
    listModule.bindCallback(ui.update);
  };

  // 处理 mainMenu 模块
  const handleMenu = () => {
    let init = menuModule.init;

    // 劫持 init 函数,这个函数完成后才能添加 UI
    Object.defineProperty(menuModule, "init", {
      get: () => {
        return (...arguments) => {
          // 等待执行完毕
          init.apply(menuModule, arguments);

          // 加载 UI
          loadUI();
        };
      },
      set: (value) => {
        init = value;
      },
    });

    // 如果已经有模块,则直接加载 UI
    if (init) {
      loadUI();
    }
  };

  // 处理 topicArg 模块
  const handleTopicModule = async () => {
    let add = topicModule.add;

    // 劫持 add 函数,这是泥潭的主题添加事件
    Object.defineProperty(topicModule, "add", {
      get: () => {
        return async (...arguments) => {
          // 主题 ID
          const tid = arguments[8];

          // 先直接隐藏,等过滤完毕后再放出来
          (() => {
            // 主题标题
            const title = document.getElementById(arguments[1]);

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

            // 隐藏元素
            container.style.display = "none";
          })();

          // 加入列表
          add.apply(topicModule, arguments);

          // 找到对应数据
          const topic = topicModule.data.find((item) => item[8] === tid);

          // 开始过滤
          await filterTopic(topic);
        };
      },
      set: (value) => {
        add = value;
      },
    });

    // 如果已经有数据,则直接过滤
    if (topicModule.data) {
      await Promise.all(Object.values(topicModule.data).map(filterTopic));
    }
  };

  // 处理 postArg 模块
  const handleReplyModule = async () => {
    let proc = replyModule.proc;

    // 劫持 proc 函数,这是泥潭的回复添加事件
    Object.defineProperty(replyModule, "proc", {
      get: () => {
        return async (...arguments) => {
          // 楼层号
          const index = arguments[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";
          })();

          // 加入列表
          proc.apply(replyModule, arguments);

          // 找到对应数据
          const reply = replyModule.data[index];

          // 开始过滤
          await filterReply(reply);
        };
      },
      set: (value) => {
        proc = value;
      },
    });

    // 如果已经有数据,则直接过滤
    if (replyModule.data) {
      await Promise.all(Object.values(replyModule.data).map(filterReply));
    }
  };

  // 处理 commonui 模块
  const handleCommonui = () => {
    // 监听 mainMenu 模块,UI 需要等待这个模块加载完成
    (() => {
      if (commonui.mainMenu) {
        menuModule = commonui.mainMenu;

        handleMenu();
        return;
      }

      Object.defineProperty(commonui, "mainMenu", {
        get: () => menuModule,
        set: (value) => {
          menuModule = value;

          handleMenu();
        },
      });
    })();

    // 监听 topicArg 模块,这是泥潭的主题入口
    (() => {
      if (commonui.topicArg) {
        topicModule = commonui.topicArg;

        handleTopicModule();
        return;
      }

      Object.defineProperty(commonui, "topicArg", {
        get: () => topicModule,
        set: (value) => {
          topicModule = value;

          handleTopicModule();
        },
      });
    })();

    // 监听 postArg 模块,这是泥潭的回复入口
    (() => {
      if (commonui.postArg) {
        replyModule = commonui.postArg;

        handleReplyModule();
        return;
      }

      Object.defineProperty(commonui, "postArg", {
        get: () => replyModule,
        set: (value) => {
          replyModule = value;

          handleReplyModule();
        },
      });
    })();
  };

  // 前置过滤
  const handlePreFilter = () => {
    // 监听 commonui 模块,这是泥潭的主入口
    (() => {
      if (unsafeWindow.commonui) {
        commonui = unsafeWindow.commonui;

        handleCommonui();
        return;
      }

      Object.defineProperty(unsafeWindow, "commonui", {
        get: () => commonui,
        set: (value) => {
          commonui = value;

          handleCommonui();
        },
      });
    })();
  };

  // 普通过滤
  const handleFilter = () => {
    const runFilter = async () => {
      if (topicModule) {
        await Promise.all(
          Object.values(topicModule.data).map((item) => {
            if (item.executed) {
              return;
            }

            item.executed = true;

            filterTopic(item);
          })
        );
      }

      if (replyModule) {
        await Promise.all(
          Object.values(replyModule.data).map((item) => {
            if (item.executed) {
              return;
            }

            item.executed = true;

            filterReply(item);
          })
        );
      }
    };

    const hookFunction = (object, functionName, callback) => {
      ((originalFunction) => {
        object[functionName] = function () {
          const returnValue = originalFunction.apply(this, arguments);

          callback.apply(this, [returnValue, originalFunction, arguments]);

          return returnValue;
        };
      })(object[functionName]);
    };

    const hook = () => {
      (() => {
        if (topicModule) {
          return;
        }

        if (commonui.topicArg) {
          topicModule = commonui.topicArg;

          hookFunction(topicModule, "add", runFilter);
        }
      })();

      (() => {
        if (replyModule) {
          return;
        }

        if (commonui.postArg) {
          replyModule = commonui.postArg;

          hookFunction(replyModule, "add", runFilter);
        }
      })();
    };

    hook();
    runFilter();

    hookFunction(commonui, "eval", hook);
  };

  // 主函数
  (() => {
    // 前置过滤
    if (preFilter) {
      handlePreFilter();
      return;
    }

    // 等待页面加载完毕后过滤
    unsafeWindow.addEventListener("load", () => {
      if (unsafeWindow.commonui === undefined) {
        return;
      }

      commonui = unsafeWindow.commonui;

      menuModule = commonui.mainMenu;

      loadUI();
      handleFilter();
    });
  })();
})();