Greasy Fork is available in English.

NGA Library

NGA 库,包括工具类、缓存、API

Ten skrypt nie powinien być instalowany bezpośrednio. Jest to biblioteka dla innych skyptów do włączenia dyrektywą meta // @require https://update.greasyfork.org/scripts/486070/1389052/NGA%20Library.js

// ==UserScript==
// @name        NGA Library
// @namespace   https://greasyfork.org/users/263018
// @version     1.0.14
// @author      snyssss
// @description NGA 库,包括工具类、缓存、API
// @license     MIT

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

// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand
// @grant       unsafeWindow
// ==/UserScript==

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

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

  /**
   * 拦截属性
   * @param {Object}    target    目标对象
   * @param {String}    property  属性或函数名称
   * @param {Function}  beforeGet 获取属性前事件
   * @param {Function}  beforeSet 设置属性前事件
   * @param {Function}  afterGet  获取属性后事件
   * @param {Function}  afterSet  设置属性前事件
   */
  static interceptProperty = (
    target,
    property,
    { beforeGet, beforeSet, afterGet, afterSet }
  ) => {
    // 判断目标对象是否存在
    if (target === undefined) {
      return;
    }

    // 判断是否已被拦截
    const isIntercepted = (() => {
      const descriptor = Object.getOwnPropertyDescriptor(target, property);

      if (descriptor && descriptor.get && descriptor.set) {
        return true;
      }

      return false;
    })();

    // 初始化目标对象的拦截列表
    target.interceptions = target.interceptions || {};
    target.interceptions[property] = target.interceptions[property] || {
      data: target[property],
      beforeGetQueue: [],
      beforeSetQueue: [],
      afterGetQueue: [],
      afterSetQueue: [],
    };

    // 写入事件
    Object.entries({
      beforeGetQueue: beforeGet,
      beforeSetQueue: beforeSet,
      afterGetQueue: afterGet,
      afterSetQueue: afterSet,
    }).forEach(([queue, event]) => {
      if (event) {
        target.interceptions[property][queue].push(event);
      }
    });

    // 拦截
    if (isIntercepted === false) {
      // 定义属性
      Object.defineProperty(target, property, {
        get: () => {
          // 获取事件
          const { data, beforeGetQueue, afterGetQueue } =
            target.interceptions[property];

          // 如果是函数
          if (this.isType(data, "function")) {
            return (...args) => {
              try {
                // 执行前操作
                // 可以在这一步修改参数
                // 可以通过在这一步抛出来阻止执行
                if (beforeGetQueue) {
                  beforeGetQueue.forEach((event) => {
                    args = event.apply(target, args);
                  });
                }

                // 执行函数
                const result = data.apply(target, args);

                // 执行后操作
                if (afterGetQueue) {
                  // 返回的可能是一个 Promise
                  const resultValue =
                    result instanceof Promise
                      ? result
                      : Promise.resolve(result);

                  resultValue.then((value) => {
                    afterGetQueue.forEach((event) => {
                      event.apply(target, [value, args, data]);
                    });
                  });
                }

                // 返回结果
                return result;
              } catch {
                return undefined;
              }
            };
          }

          try {
            // 返回前操作
            // 可以在这一步修改返回结果
            // 可以通过在这一步抛出来返回 undefined
            let result = data;

            if (beforeGetQueue) {
              beforeGetQueue.forEach((event) => {
                result = event.apply(target, [result]);
              });
            }

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

            // 返回结果
            return result;
          } catch {
            return undefined;
          }
        },
        set: (value) => {
          // 获取事件
          const { data, beforeSetQueue, afterSetQueue } =
            target.interceptions[property];

          // 声明结果
          let result = value;

          try {
            // 写入前操作
            // 可以在这一步修改写入结果
            // 可以通过在这一步抛出来写入 undefined
            if (beforeSetQueue) {
              beforeSetQueue.forEach((event) => {
                result = event.apply(target, [data, result]);
              });
            }

            // 写入可能的事件
            if (this.isType(data, "object")) {
              result.interceptions = data.interceptions;
            }

            // 写入后操作
            if (afterSetQueue) {
              afterSetQueue.forEach((event) => {
                event.apply(target, [result, value]);
              });
            }
          } catch {
            result = undefined;
          } finally {
            // 写入结果
            target.interceptions[property].data = result;

            // 返回结果
            return result;
          }
        },
      });
    }

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

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

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

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

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

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

    return target;
  };

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

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

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

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

      return 0;
    });

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

      const reader = new FileReader();

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

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

        resolve(text);
      };

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

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

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

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

            if (nextIndex < 0) {
              break;
            }
          }

          nextIndex = nextIndex + 1;
        }
      }

      return -1;
    };

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

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

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

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

    return null;
  };

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

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

      return h;
    })();

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

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

    const rgb = ((h, s, v) => {
      const f = (n, k = (n + h / 60) % 6) =>
        v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);

      return [f(5), f(3), f(1)];
    })(hsv[0], hsv[1], hsv[2]);

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

  /**
   * 计算时间是否为今天
   * @param   {Date}    date 时间
   * @returns {Boolean}
   */
  static dateIsToday(date) {
    const now = new Date();

    return (
      date.getFullYear() === now.getFullYear() &&
      date.getMonth() === now.getMonth() &&
      date.getDate() === now.getDate()
    );
  }

  /**
   * 计算时间差
   * @param   {Date}    start 开始时间
   * @param   {Date}    end   结束时间
   * @returns {object}        时间差
   */
  static dateDiff(start, end = new Date()) {
    if (start > end) {
      return dateDiff(end, start);
    }

    const startYear = start.getFullYear();
    const startMonth = start.getMonth();
    const startDay = start.getDate();

    const endYear = end.getFullYear();
    const endMonth = end.getMonth();
    const endDay = end.getDate();

    const diff = {
      years: endYear - startYear,
      months: endMonth - startMonth,
      days: endDay - startDay,
    };

    const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    if (
      startYear % 400 === 0 ||
      (startYear % 100 !== 0 && startYear % 4 === 0)
    ) {
      daysInMonth[1] = 29;
    }

    if (diff.months < 0) {
      diff.years -= 1;
      diff.months += 12;
    }

    if (diff.days < 0) {
      if (diff.months === 0) {
        diff.years -= 1;
        diff.months = 11;
      } else {
        diff.months -= 1;
      }

      diff.days += daysInMonth[startMonth];
    }

    return diff;
  }
}

/**
 * 初始化缓存和 API
 */
const initCacheAndAPI = (() => {
  // KEY
  const USER_AGENT_KEY = "USER_AGENT_KEY";
  const CLEAR_TIME_KEY = "CLEAR_TIME_KEY";

  /**
   * 数据库名称
   */
  const name = "NGA_Storage";

  /**
   * 模块列表
   */
  const modules = {
    TOPIC_NUM_CACHE: {
      keyPath: "uid",
      version: 1,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60,
      persistent: true,
    },
    USER_INFO_CACHE: {
      keyPath: "uid",
      version: 1,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60,
      persistent: false,
    },
    USER_IPLOC_CACHE: {
      keyPath: "uid",
      version: 1,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60,
      persistent: true,
    },
    PAGE_CACHE: {
      keyPath: "url",
      version: 1,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 10,
      persistent: false,
    },
    FORUM_POSTED_CACHE: {
      keyPath: "url",
      version: 1,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60 * 24,
      persistent: true,
    },
    USER_NAME_CHANGED: {
      keyPath: "uid",
      version: 2,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60 * 24,
      persistent: true,
    },
    USER_STEAM_INFO: {
      keyPath: "uid",
      version: 3,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60 * 24,
      persistent: true,
    },
    USER_PSN_INFO: {
      keyPath: "uid",
      version: 3,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60 * 24,
      persistent: true,
    },
    USER_NINTENDO_INFO: {
      keyPath: "uid",
      version: 3,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60 * 24,
      persistent: true,
    },
    USER_GENSHIN_INFO: {
      keyPath: "uid",
      version: 3,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60 * 24,
      persistent: false,
    },
    USER_SKZY_INFO: {
      keyPath: "uid",
      version: 3,
      indexes: ["timestamp"],
      expireTime: 1000 * 60 * 60 * 24,
      persistent: false,
    },
  };

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

  class DBStorage {
    /**
     * 当前实例
     */
    instance = null;

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

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

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

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

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

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

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

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

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

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

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

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

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

      return transaction.objectStore(name);
    }

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

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

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

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

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

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

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

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

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

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

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

      // 返回结果
      return result;
    }

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

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

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

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

      // 返回结果
      return result;
    }

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

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

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

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

      // 返回结果
      return result;
    }

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

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

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

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

      // 返回结果
      return result;
    }

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

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

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

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

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

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

        return -1;
      }

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

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

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

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

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

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

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

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

        // 返回结果
        return result;
      }

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

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

      return result;
    }
  }

  /**
   * 油猴存储
   *
   * 虽然使用了不支持 Promise 的 GM_getValue 与 GM_setValue,但是为了配合 IndexedDB,统一视为 Promise
   */
  class GMStorage extends DBStorage {
    /**
     * 插入指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async add(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(modules, name) === false) {
        return GM_setValue(name, data);
      }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return true;
      });

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

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

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

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

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

        return -1;
      }

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

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

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

        return true;
      });

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

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

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

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

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

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

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

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

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

        return true;
      });

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

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

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

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

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

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

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

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

      return result;
    }
  }

  /**
   * 缓存管理
   *
   * 在存储的基础上,增加了过期时间和持久化选项,自动清理缓存
   */
  class Cache extends GMStorage {
    /**
     * 插入指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async add(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(modules, name)) {
        data.timestamp = data.timestamp || new Date().getTime();
      }

      return super.add(name, data);
    }

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

      return super.put(name, data);
    }

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

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

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

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

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

      return null;
    }

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

      return super.bulkAdd(name, data);
    }

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

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

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

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

      return keys.length - result.length;
    }

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

      return super.bulkPut(name, data);
    }

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

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

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

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

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

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

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

      // 返回结果
      return result;
    }
  }

  /**
   * API
   */
  class API {
    /**
     * 缓存管理
     */
    cache;

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

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

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

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

      return result;
    }

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

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

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

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

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

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

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

        if (cache) {
          return cache.count;
        }

        return 0;
      })();

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

      return count;
    }

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

      const api = `nuke.php?func=ucp&uid=${uid}`;

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

      if (cache) {
        return cache.data;
      }

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

      const data = (() => {
        const text = Tools.searchPair(result, `__UCPUSER =`);

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

        return null;
      })();

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

      return data || {};
    }

    /**
     * 获取属地列表
     * @param {number} uid 用户 ID
     */
    async getIpLocations(uid) {
      const name = "USER_IPLOC_CACHE";
      const { expireTime } = modules[name];

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

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

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

      // 属地列表
      const data = cache ? cache.data : [];

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

      // 写入缓存
      if (ipLoc) {
        const index = data.findIndex((item) => {
          return item.ipLoc === ipLoc;
        });

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

        data.unshift({
          ipLoc,
          timestamp: new Date().getTime(),
        });

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

      // 返回结果
      return data;
    }

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

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

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

      if (cache) {
        return cache.data;
      }

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

      const parser = new DOMParser();

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

      // 声明返回值
      const data = {
        subject: "",
        content: "",
        userInfo: null,
        reputation: NaN,
      };

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

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

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

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

          return 0;
        })();

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

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

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

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

            return null;
          })();

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

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

              return null;
            })();

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

            return NaN;
          })();
        }
      }

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

      // 返回结果
      return data;
    }

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

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

      const result = await this.request(api);

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

      return info;
    }

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

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

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

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

        return cache.data;
      }

      let isComplete = false;
      let isBusy = false;

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

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

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

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

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

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

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

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

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

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

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

      return isComplete;
    }

    /**
     * 获取用户的曾用名
     * @param {number} uid 用户 ID
     */
    async getUsernameChanged(uid) {
      const name = "USER_NAME_CHANGED";
      const { expireTime } = modules[name];

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

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

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

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

      // 请求用户信息
      const { usernameChanged } = await this.getUserInfo(uid);

      // 如果有修改记录
      if (usernameChanged) {
        // 请求数据
        const result = await this.request(api);

        // 取得结果
        const data = result.data ? result.data[0] : null;

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

        return data;
      }

      return null;
    }

    /**
     * 获取用户绑定的 Steam 信息
     * @param {number} uid 用户 ID
     */
    async getSteamInfo(uid) {
      const name = "USER_STEAM_INFO";
      const { expireTime } = modules[name];

      const api = `/nuke.php?lite=js&__lib=steam&__act=steam_user_info&user_id=${uid}`;

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

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

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

      // 请求数据
      // Steam ID 64 位会超出 JavaScript Number 长度,需要手动处理
      const result = await this.request(
        api,
        {
          method: "POST",
        },
        false
      );

      // 先转换成 JSON
      const resultJSON = JSON.parse(result);

      // 取得结果
      const data = resultJSON.data ? resultJSON.data[0] : null;

      // 如果有绑定的数据,从原始数据中取得数据,并转为 String 格式
      if (data.steam_user_id) {
        const matched = result.match(/"steam_user_id":(\d+),/);

        if (matched) {
          data.steam_user_id = String(matched[1]);
        }
      }

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

      return data;
    }

    /**
     * 获取用户绑定的 PSN 信息
     * @param {number} uid 用户 ID
     */
    async getPSNInfo(uid) {
      const name = "USER_PSN_INFO";
      const { expireTime } = modules[name];

      const api = `/nuke.php?lite=js&__lib=psn&__act=psn_user_info&user_id=${uid}`;

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

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

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

      // 请求数据
      // PSN ID 64 位会超出 JavaScript Number 长度,需要手动处理
      const result = await this.request(
        api,
        {
          method: "POST",
        },
        false
      );

      // 先转换成 JSON
      const resultJSON = JSON.parse(result);

      // 取得结果
      const data = resultJSON.data ? resultJSON.data[0] : null;

      // 如果有绑定的数据,从原始数据中取得数据,并转为 String 格式
      if (data.psn_user_id) {
        const matched = result.match(/"psn_user_id":(\d+),/);

        if (matched) {
          data.psn_user_id = String(matched[1]);
        }
      }

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

      return data;
    }

    /**
     * 获取用户绑定的 NS 信息
     * @param {number} uid 用户 ID
     */
    async getNintendoInfo(uid) {
      const name = "USER_NINTENDO_INFO";
      const { expireTime } = modules[name];

      const api = `/nuke.php?lite=js&__lib=nintendo&__act=user_info&user_id=${uid}`;

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

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

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

      // 请求数据
      const result = await this.request(api, {
        method: "POST",
      });

      // 取得结果
      const data = result.data ? result.data[0] : null;

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

      return data;
    }

    /**
     * 获取用户绑定的原神信息
     * @param {number} uid 用户 ID
     */
    async getGenshinInfo(uid) {
      const name = "USER_GENSHIN_INFO";
      const { expireTime } = modules[name];

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

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

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

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

      // 请求数据
      const result = await this.request(api, {
        method: "POST",
      });

      // 取得结果
      const data = result.data ? result.data[0] : null;

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

      return data;
    }

    /**
     * 获取用户绑定的深空之眼信息
     * @param {number} uid 用户 ID
     */
    async getSKZYInfo(uid) {
      const name = "USER_SKZY_INFO";
      const { expireTime } = modules[name];

      const api = `/nuke.php?lite=js&__lib=auth_ys4fun&__act=skzy_user_game&user_id=${uid}`;

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

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

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

      // 请求数据
      const result = await this.request(api, {
        method: "POST",
      });

      // 取得结果
      const data = result.data ? result.data[0] : null;

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

      return data;
    }

    /**
     * 获取用户绑定的游戏信息
     * @param {number} uid 用户 ID
     */
    async getUserGameInfo(uid) {
      // 请求 Steam 信息
      const steam = await this.getSteamInfo(uid);

      // 请求 PSN 信息
      const psn = await this.getPSNInfo(uid);

      // 请求 NS 信息
      const nintendo = await this.getNintendoInfo(uid);

      // 请求原神信息
      const genshin = await this.getGenshinInfo(uid);

      // 请求深空之眼信息
      const skzy = await this.getSKZYInfo(uid);

      // 返回结果
      return {
        steam,
        psn,
        nintendo,
        genshin,
        skzy,
      };
    }
  }

  /**
   * 注册脚本菜单
   * @param {Cache} cache 缓存管理
   */
  const registerMenu = async (cache) => {
    const data = (await cache.get(USER_AGENT_KEY)) || "Nga_Official";

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

      if (value) {
        cache.put(USER_AGENT_KEY, value);
        location.reload();
      }
    });
  };

  /**
   * 自动清理缓存
   * @param {Cache} cache 缓存管理
   */
  const autoClear = async (cache) => {
    const data = await cache.get(CLEAR_TIME_KEY);

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

    const isToday = Tools.dateIsToday(clearTime);

    if (isToday) {
      return;
    }

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

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

  // 初始化事件
  return () => {
    // 防止重复初始化
    if (unsafeWindow.NLibrary === undefined) {
      // 初始化缓存和 API
      const cache = new Cache();
      const api = new API(cache);

      // 自动清理缓存
      autoClear(cache);

      // 写入全局变量
      unsafeWindow.NLibrary = {
        cache,
        api,
      };
    }

    const { cache, api } = unsafeWindow.NLibrary;

    // 注册脚本菜单
    registerMenu(cache);

    // 返回结果
    return {
      cache,
      api,
    };
  };
})();