NGA Cache History

将帖子内容缓存 IndexedDB 里,以便在帖子被审核/删除时仍能查看

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        NGA Cache History
// @name:zh-CN  NGA 帖子缓存插件
// @namespace   https://greasyfork.org/users/263018
// @version     1.2.4
// @author      snyssss
// @description 将帖子内容缓存 IndexedDB 里,以便在帖子被审核/删除时仍能查看
// @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

// @noframes
// ==/UserScript==
(async ({ commonui: ui, _LOADERREAD: loader }) => {
  // 检查是否支持 IndexedDB
  if (window.indexedDB === undefined) {
    return;
  }

  // 常量
  const VERSION = 1;
  const DB_NAME = "NGA_CACHE";
  const TABLE_NAME = "reads";
  const SHOW_DIFFRENCE_KEY = "SHOW_DIFFRENCE";
  const EXPIRE_DURATION_KEY = "EXPIRE_DURATION";
  const REFETCH_NOTIFICATION_INTERVAL_KEY = "REFETCH_NOTIFICATION_INTERVAL";

  // 显示差异
  const SHOW_DIFFRENCE = GM_getValue(SHOW_DIFFRENCE_KEY, false);

  // 缓存时长
  const EXPIRE_DURATION = GM_getValue(EXPIRE_DURATION_KEY, 7);

  // 获取提示信息间隔
  const REFETCH_NOTIFICATION_INTERVAL = GM_getValue(
    REFETCH_NOTIFICATION_INTERVAL_KEY,
    10
  );

  // 判断帖子是否正常
  const isSuccess = () => {
    return ui;
  };

  // 格式化 URL
  const formatUrl = (url) => {
    // 分割 URL
    const urlSplit = url.split("?");

    // 获取页面参数
    const params = new URLSearchParams(urlSplit[1]);

    // 如果是第一页,移除页码参数
    if (params.get("page") === "1") {
      params.delete("page");
    }

    // 移除 _ff 参数
    params.delete("_ff");

    // 返回格式化后的结果
    return `${urlSplit[0]}?${params.toString()}`;
  };

  // 获取首页 URL
  const getHeadUrl = (url) => {
    // 格式化 URL
    url = formatUrl(url);

    // 分割 URL
    const urlSplit = url.split("?");

    // 获取页面参数
    const params = new URLSearchParams(urlSplit[1]);

    // 获取 TID
    const tid = params.get("tid");

    // 返回首页 URL
    return `${urlSplit[0]}?tid=${tid}`;
  };

  // 获取数据库实例
  const db = await new Promise((resolve) => {
    // 打开 IndexedDB 数据库
    const request = window.indexedDB.open(DB_NAME, VERSION);

    // 如果数据库不存在则创建
    request.onupgradeneeded = (event) => {
      // 创建表
      const store = event.target.result.createObjectStore(TABLE_NAME, {
        keyPath: "url",
      });

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

    // 成功后返回实例
    request.onsuccess = (event) => {
      resolve(event.target.result);
    };
  });

  // 获取数据
  const get = (url, onsuccess, onerror = () => {}) => {
    // 格式化 URL
    url = formatUrl(url);

    // 只缓存帖子内容
    if (url.indexOf("/read.php") < 0) {
      return;
    }

    // 创建事务
    const transaction = db.transaction([TABLE_NAME], "readonly");

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

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

    // 成功后处理数据
    request.onsuccess = (event) => {
      // 获取页面对象
      const data = event.target.result;

      // 不存在则抛出异常
      if (data === undefined) {
        onerror();
        return;
      }

      // 处理数据
      onsuccess(data);
    };

    // 失败后抛出异常
    request.onerror = () => {
      onerror();
    };
  };

  // 删除超时数据
  const expire = (offset) => {
    // 创建事务
    const transaction = db.transaction([TABLE_NAME], "readwrite");

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

    // 获取索引
    const index = store.index("timestamp");

    // 查找超时数据
    const request = index.openCursor(
      IDBKeyRange.upperBound(Date.now() - offset)
    );

    // 成功后删除数据
    request.onsuccess = (event) => {
      const cursor = event.target.result;

      if (cursor) {
        store.delete(cursor.primaryKey);

        cursor.continue();
      }
    };
  };

  // 删除数据
  const remove = (url, onsuccess = () => {}, onerror = () => {}) => {
    // 格式化 URL
    url = formatUrl(url);

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

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

    // 删除数据
    const request = store.delete(url);

    // 成功后回调
    request.onsuccess = () => {
      onsuccess();
    };

    // 失败后回调
    request.onerror = () => {
      onerror();
    };
  };

  // 写入数据
  const put = (url, data, onsuccess = () => {}, onerror = () => {}) => {
    // 格式化 URL
    url = formatUrl(url);

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

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

    // 写入数据
    const request = store.put({
      url,
      timestamp: Date.now(),
      ...data,
    });

    // 成功后回调
    request.onsuccess = () => {
      onsuccess();
    };

    // 失败后回调
    request.onerror = () => {
      onerror();
    };
  };

  // 缓存数据
  const save = (url) => {
    // 格式化 URL
    url = formatUrl(url);

    // 只缓存帖子内容
    if (url.indexOf("/read.php") < 0) {
      return;
    }

    // 重新请求原始数据用于缓存
    fetch(url)
      .then((res) => res.blob())
      .then((res) => {
        // 读取内容
        const reader = new FileReader();

        reader.onload = async () => {
          // 读取内容
          const content = reader.result;

          // 解析标题
          const parser = new DOMParser();
          const html = parser.parseFromString(content, "text/html");
          const title = (() => {
            const str = html.querySelector("title").textContent;
            const index = str.lastIndexOf(" ");

            if (index > 0) {
              return str.substring(0, index);
            }

            return str;
          })();

          // 没有楼层,说明卡审核
          if (content.indexOf("commonui.postArg.proc(") < 0) {
            return;
          }

          // 找到 ID 是 postcontainer 开头的元素
          const containers = html.querySelectorAll("[id^=postcontainer]");

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

          // 有锚点,但是找不到楼层,也是卡审核
          const anchor = url.match(/(#pid\d+Anchor)$/);

          if (anchor && html.querySelector(anchor[1]) === null) {
            return;
          }

          // 如果未开启浏览记录,直接写入缓存
          if (SHOW_DIFFRENCE === false) {
            put(url, {
              title,
              content,
            });
          }
          // 否则判断是否是正常的翻页,如果是则需要更新最大楼层数
          else {
            // 分割 URL
            const urlSplit = url.split("?");

            // 获取页面参数
            const params = new URLSearchParams(urlSplit[1]);

            // 移除 TID 参数
            params.delete("tid");

            // 移除页码参数
            params.delete("page");

            // 如果仍有参数,只缓存当前页,无需更新最大楼层数
            if (params.size > 0) {
              put(url, {
                title,
                content,
              });
            }
            // 否则需要更新最大楼层数
            else {
              // 获取首页 URL
              const headUrl = getHeadUrl(url);

              // 当前页不是首页,写入缓存
              if (headUrl !== url) {
                put(url, {
                  title,
                  content,
                });
              }

              // 获取当前页面的最大楼层数
              const count = parseInt(
                containers[containers.length - 1]
                  .getAttribute("id")
                  .replace("postcontainer", ""),
                10
              );

              // 获取首页缓存
              get(
                headUrl,
                (data) => {
                  // 获取缓存楼层数
                  const cache = data.rows || 0;

                  // 计算最大楼层数
                  const max = Math.max(count, cache);

                  // 当前页是首页,直接更新缓存
                  if (headUrl === url) {
                    put(url, {
                      title,
                      content,
                      rows: max,
                    });

                    loadAction();
                    return;
                  }

                  // 如果与缓存的最大楼层数相同,无需更新
                  if (max === cache) {
                    return;
                  }

                  // 更新缓存
                  put(headUrl, {
                    ...data,
                    rows: max,
                  });
                },
                () => {
                  // 当前页是首页,直接更新缓存
                  if (headUrl === url) {
                    put(url, {
                      title,
                      content,
                      rows: count,
                    });

                    loadAction();
                  }
                }
              );
            }
          }
        };

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

  // 读取数据
  const load = (url, document) => {
    // 格式化 URL
    url = formatUrl(url);

    return get(url, (data) => {
      // 加载缓存内容
      const html = document.open("text/html", "replace");

      html.write(data.content);
      html.close();

      // 缓存时间格式
      const formatedDate = (() => {
        const date = new Date(data.timestamp);
        const year = date.getFullYear();
        const month = ("0" + (date.getMonth() + 1)).slice(-2);
        const day = ("0" + date.getDate()).slice(-2);
        const hours = ("0" + date.getHours()).slice(-2);
        const minutes = ("0" + date.getMinutes()).slice(-2);

        return `${year}-${month}-${day} ${hours}:${minutes}`;
      })();

      // 写入缓存时间
      (() => {
        const execute = () => {
          const container = document.querySelector('td[id^="postcontainer"]');

          if (container) {
            const elements = container.querySelectorAll(":scope > .clear");

            const anchor = elements[elements.length - 1];

            if (anchor) {
              anchor.insertAdjacentHTML(
                "afterend",
                `<h4 class="silver subtitle">缓存</h4><span class="block_txt block_txt_c3">${formatedDate}</span>`
              );
              return;
            }
          }

          setTimeout(execute, 160);
        };

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

  // STYLE
  GM_addStyle(`
      .s-table-wrapper {
        height: calc((2em + 10px) * 11 + 3px);
        overflow-y: auto;
      }
      .s-table {
        margin: 0;
      }
      .s-table th,
      .s-table td {
        position: relative;
        white-space: nowrap;
      }
      .s-table th {
        position: sticky;
        top: 2px;
        z-index: 1;
      }
      .s-text-ellipsis > * {
        flex: 1;
        width: 1px;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    `);

  // UI
  const loadUI = () => {
    if (!ui) {
      return;
    }

    const content = (() => {
      const c = document.createElement("div");

      c.innerHTML = `
        <div class="s-table-wrapper" style="width: 1000px; max-width: 95vw;">
          <table class="s-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1" width="1">时间</th>
                <th class="c2">内容</th>
                <th class="c3" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
        <div style="display: flex; margin-top: 10px;">
          <input type="text" style="flex: 1;" placeholder="目前支持通过帖子链接或标题进行筛选,查询旧数据可能需要一些时间" />
          <button>筛选</button>
        </div>
      `;

      return c;
    })();

    let position = null;
    let hasNext = true;
    let isFetching = false;
    let keyword = "";

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

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

    const keywordInput = content.querySelector("INPUT");

    const filterButton = content.querySelector("BUTTON");

    const fetchData = () => {
      isFetching = true;

      // 声明查询数量
      let limit = 10;

      // 创建事务
      const transaction = db.transaction([TABLE_NAME], "readonly");

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

      // 获取索引
      const index = store.index("timestamp");

      // 查找数据
      const request = index.openCursor(
        position ? IDBKeyRange.upperBound(position) : null,
        "prev"
      );

      // 加载列表
      request.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor) {
          const { url, title, timestamp } = cursor.value;

          position = timestamp;

          if (list.querySelector(`[data-url="${url}"]`)) {
            cursor.continue();
            return;
          }

          if (keyword) {
            if (url.indexOf(keyword) < 0 && title.indexOf(keyword) < 0) {
              cursor.continue();
              return;
            }
          }

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

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

          item.setAttribute("data-url", url);

          item.innerHTML = `
            <td class="c1">
              <span class="nobr">${ui.time2dis(timestamp / 1000)}</span>
            </td>
            <td class="c2">
              <div class="s-text-ellipsis">
                <span>
                  <a href="${url}" title="${title}" class="b nobr">${title}</a>
                </span>
              </div>
            </td>
            <td class="c3">
              <button>查看缓存版本</button>
              <button>删除</button>
            </td>
          `;

          const buttons = item.querySelectorAll("button");

          // 查看缓存版本
          buttons[0].onclick = () => {
            const iWindow = ui.createCommmonWindow();
            const iframe = document.createElement("IFRAME");

            iframe.style.width = "80vw";
            iframe.style.height = "80vh";
            iframe.style.border = "none";

            const iframeLoad = () => {
              iframe.removeEventListener("load", iframeLoad);

              load(url, iframe.contentDocument);
            };

            iframe.addEventListener("load", iframeLoad);

            iWindow._.addTitle(title);
            iWindow._.addContent(iframe);
            iWindow._.show();
          };

          // 删除缓存
          buttons[1].onclick = () => {
            remove(url, () => {
              list.removeChild(item);

              if (list.childElementCount < 10) {
                fetchData();
              }
            });
          };

          list.appendChild(item);

          if (limit > 1) {
            cursor.continue();
          } else {
            isFetching = false;
          }
        } else {
          hasNext = false;
        }

        limit -= 1;
      };
    };

    const refetch = (value = ``) => {
      list.innerHTML = ``;

      position = null;
      hasNext = true;
      isFetching = false;
      keyword = value;

      keywordInput.value = value;

      fetchData();
    };

    wrapper.onscroll = () => {
      if (isFetching || !hasNext) {
        return;
      }

      if (
        wrapper.scrollHeight - wrapper.scrollTop <=
        wrapper.clientHeight * 1.1
      ) {
        fetchData();
      }
    };

    filterButton.onclick = () => {
      refetch(keywordInput.value);
    };

    // 增加菜单项
    (() => {
      const title = "浏览记录";

      let window;

      ui.mainMenu.addItemOnTheFly(title, null, () => {
        if (window === undefined) {
          window = ui.createCommmonWindow();
        }

        refetch();

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

  // 加载操作按钮
  // 目前只有主楼的删除缓存
  const loadAction = () => {
    if (ui && ui.postArg) {
      const { data } = ui.postArg;

      if (data && data["0"] && data["0"]["pid"] === 0) {
        const item = data["0"];
        const pInfoC = item["pInfoC"];
        const anchor = pInfoC.querySelector(`[title="操作菜单"]`);

        const action = pInfoC.querySelector(`[title="缓存"]`);

        if (anchor && action === null) {
          const element = document.createElement("A");

          element.href = "javascript:void(0)";
          element.className = `postinfob postfavb postoptb small_colored_text_btn block_txt_c0 stxt`;
          element.title = "缓存";

          element.append(...__TXT.svg("turned_in", "", 8));

          element.onclick = () => {
            const url = window.location.href;

            // 判断是否已有缓存
            // 目前默认缓存所有页面,所以一定会有缓存
            const cached = element.classList.contains("postoptb");

            if (cached) {
              remove(url, () => {
                element.classList.remove("postoptb");
              });
            } else {
              save(url);

              element.classList.add("postoptb");
            }
          };

          anchor.parentElement.insertBefore(element, anchor);
        }
      }
    }
  };

  // 加载消息
  const loadMessage = () => {
    if (!ui) {
      return;
    }

    // 获取消息并写入缓存
    const execute = () => {
      fetch("/nuke.php?lite=js&__lib=noti&__act=get_all")
        .then((res) => res.blob())
        .then((blob) => {
          const reader = new FileReader();

          reader.onload = () => {
            const text = reader.result;
            const result = JSON.parse(
              text
                .replace("window.script_muti_get_var_store=", "")
                .replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2": ')
            );

            if (result.data) {
              const data = result.data[0];

              const list = ["0", "1", "2"].reduce(
                (res, key) => ({
                  ...res,
                  [key]: data[key],
                }),
                {}
              );

              // 有未读消息,说明抢先获取了,需要弹出提醒
              if (data.unread) {
                for (let type in list) {
                  const group = list[type];

                  if (!group) {
                    continue;
                  }

                  for (let i = 0; i < group.length; i += 1) {
                    const item = group[i];

                    if (!item) {
                      continue;
                    }

                    if (i < group.length - 5) {
                      continue;
                    }

                    ui.notification._add(type, item);
                  }

                  if (group.length > 5) {
                    ui.notification._more.style.display = "";
                  }
                }

                ui.notification.openBox();
              }

              // 处理缓存
              // 只处理 0,也就是 _BIT_REPLY 的情况
              if (list["0"]) {
                const group = list["0"];

                for (let i = 0; i < group.length; i += 1) {
                  const item = group[i];

                  if (!item) {
                    continue;
                  }

                  // 消息的时间
                  const time = item[9] * 1000;

                  // 消息的内容,参考 js_notification.js 的 TPL
                  let str = TPL[0][item[0]];

                  if (typeof str == "function") {
                    str = str(item);
                  }

                  str = str
                    .replace(/\{(_[A-Z0-9_]+)\}/g, function ($0, $1) {
                      return TPLSUB[$1] ? TPLSUB[$1] : $0;
                    })
                    .replace(/\{(_[A-Z0-9_]+)\}/g, function ($0, $1) {
                      return item[KEY[$1]] ? item[KEY[$1]] : $0;
                    });

                  // 获取里面出现的所有页面链接
                  const urls = [
                    ...str.matchAll(/href="(\/read.php[^"]*)"/gi),
                  ].map((match) => `${window.location.origin}${match[1]}`);

                  for (let index in urls) {
                    // 链接地址
                    const url = urls[index];

                    // 创建事务
                    const transaction = db.transaction(
                      [TABLE_NAME],
                      "readonly"
                    );

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

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

                    // 成功后处理数据
                    request.onsuccess = (event) => {
                      // 获取页面对象
                      const data = event.target.result;

                      // 存在,且缓存的时间晚于消息时间则跳过
                      if (data && data.timestamp > time) {
                        return;
                      }

                      // 写入缓存
                      save(url);
                    };
                  }
                }
              }
            }
          };

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

    // NGA 的消息机制是在页面加载的时候由服务端写在页面里再请求消息
    // 这会导致页面不刷新的时候,收到的提醒不能及时获知,等刷新时帖子可能已经没了
    // 所以需要定时获取最新消息,保证不刷论坛的情况下也会缓存提醒
    // 泥潭审核机制导致有消息提示但是找不到帖子的情况待解决
    const excuteInterval = () => {
      if (REFETCH_NOTIFICATION_INTERVAL > 0) {
        execute();
        setInterval(execute, REFETCH_NOTIFICATION_INTERVAL * 60 * 1000);
      }
    };

    // 启动定时器
    if (ui.notification) {
      excuteInterval();
    } else {
      ui.loadNotiScript(excuteInterval);
    }
  };

  // 绑定事件
  const hook = () => {
    // 钩子
    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]);
    };

    // 页面跳转
    if (loader) {
      hookFunction(loader, "go", (returnValue, originalFunction, arguments) => {
        if (arguments[1]) {
          const { url } = arguments[1];

          save(url);
        }
      });
    }

    // 快速翻页
    if (ui) {
      hookFunction(
        ui,
        "loadReadHidden",
        (returnValue, originalFunction, arguments) => {
          if (arguments && __PAGE) {
            const p = (() => {
              if (arguments[1] & 2) {
                return __PAGE[2] + 1;
              }

              if (arguments[1] & 4) {
                return __PAGE[2] - 1;
              }

              return arguments[0];
            })();

            if (p < 1 || (__PAGE[1] > 0 && p > __PAGE[1])) {
              return;
            }

            const urlParams = new URLSearchParams(window.location.search);

            urlParams.set("page", p);

            const url = `${window.location.origin}${
              window.location.pathname
            }?${urlParams.toString()}`;

            save(url);
          }
        }
      );
    }

    // 显示浏览记录或恢复帖子列表里异常的帖子
    if (ui && ui.topicArg) {
      const execute = () => {
        ui.topicArg.data.forEach((item) => {
          const tid = item[8];
          const postDate = item[12];

          const url = `${window.location.origin}/read.php?tid=${tid}`;

          get(url, (data) => {
            if (postDate > 0) {
              if (SHOW_DIFFRENCE) {
                const replies = parseInt(item[0].innerHTML, 10);
                const rows = data.rows === undefined ? replies : data.rows;

                const diffrence = replies - rows;

                if (diffrence > 0) {
                  const page = Math.ceil(rows / 20);

                  if (page > 1) {
                    item[0].setAttribute("href", `${url}&page=${page}`);
                  }

                  item[0].innerHTML = `${replies}<small>(+${diffrence})</small>`;
                }

                item[1].style.opacity = "0.5";
              }
              return;
            }

            item[1].innerHTML = data.title;
            item[2].innerHTML = "缓存";
            item[3].innerHTML = ui.time2dis(data.timestamp / 1000);
          });
        });
      };

      hookFunction(ui.topicArg, "loadAll", execute);
      execute();
    }
  };

  // 加载菜单项
  (() => {
    GM_registerMenuCommand(
      `浏览记录:${SHOW_DIFFRENCE ? "显示" : "关闭"}`,
      () => {
        GM_setValue(SHOW_DIFFRENCE_KEY, !SHOW_DIFFRENCE);
        location.reload();
      }
    );

    GM_registerMenuCommand(`缓存天数:${EXPIRE_DURATION} 天`, () => {
      const input = prompt("请输入缓存天数(最大1000):", EXPIRE_DURATION);

      if (input) {
        const value = parseInt(input, 10);

        if (value > 0 && value <= 1000) {
          GM_setValue(EXPIRE_DURATION_KEY, value);

          location.reload();
        }
      }
    });

    GM_registerMenuCommand(
      `消息刷新间隔:${REFETCH_NOTIFICATION_INTERVAL} 分钟`,
      () => {
        const input = prompt(
          "请输入消息刷新间隔(单位:分钟,设置为 0 的时候不启用):",
          REFETCH_NOTIFICATION_INTERVAL
        );

        if (input) {
          const value = parseInt(input, 10);

          if (value <= 1440) {
            GM_setValue(REFETCH_NOTIFICATION_INTERVAL_KEY, value);

            location.reload();
          }
        }
      }
    );
  })();

  // 执行脚本
  (() => {
    // 绑定事件
    hook();

    // 删除超时数据
    expire(EXPIRE_DURATION * 24 * 60 * 60 * 1000);

    // 加载UI
    loadUI();

    // 加载消息
    loadMessage();

    // 当前链接地址
    const url = window.location.href;

    // 帖子正常的情况下缓存数据,否则尝试从缓存中读取
    if (isSuccess()) {
      save(url);
    } else {
      load(url, document);
    }
  })();
})(unsafeWindow);