Greasy Fork is available in English.

NGA Library

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

Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyfork.org/scripts/486070/1416483/NGA%20Library.js

// ==UserScript==
// @name        NGA Library
// @namespace   https://greasyfork.org/users/263018
// @version     1.1.4
// @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   {String}           css 样式信息
   * @param   {String}           id  样式 ID
   * @returns {HTMLStyleElement}     样式元素
   */
  static addStyle(css, id = "s-" + Math.random().toString(36).slice(2)) {
    let element = document.getElementById(id);

    if (element === null) {
      element = document.createElement("STYLE");
      element.id = id;

      document.head.appendChild(element);
    }

    element.textContent = css;

    return element;
  }

  /**
   * 计算时间是否为今天
   * @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;
  }
}

/**
 * 简单队列
 */
class Queue {
  /**
   * 任务队列
   */
  queue = {};

  /**
   * 当前状态 - IDLE, RUNNING, PAUSED
   */
  state = "IDLE";

  /**
   * 异常暂停时间
   */
  pauseTime = 1000 * 60 * 5;

  /**
   * 添加任务
   * @param {string}        key     标识
   * @param {() => Promise} task    任务
   */
  enqueue(key, task) {
    if (Object.hasOwn(this.queue, key)) {
      return;
    }

    this.queue[key] = task;
    this.run();
  }

  /**
   * 移除任务
   * @param {string} key 标识
   */
  dequeue(key) {
    if (Object.hasOwn(this.queue, key) === false) {
      return;
    }

    delete this.queue[key];
  }

  /**
   * 执行任务
   */
  run() {
    // 非空闲状态,直接返回
    if (this.state !== "IDLE") {
      return;
    }

    // 获取任务队列标识
    const keys = Object.keys(this.queue);

    // 任务队列为空,直接返回
    if (keys.length === 0) {
      return;
    }

    // 标记为执行中
    this.state = "RUNNING";

    // 取得第一个任务
    const key = keys[0];

    // 执行任务
    this.queue[key]()
      .then(() => {
        // 移除任务
        this.dequeue(key);
      })
      .catch(async () => {
        // 标记为暂停
        this.state = "PAUSED";

        // 等待指定时间
        await new Promise((resolve) => {
          setTimeout(resolve, this.pauseTime);
        });
      })
      .finally(() => {
        // 标记为空闲
        this.state = "IDLE";

        // 执行下一个任务
        this.run();
      });
  }
}

/**
 * 初始化缓存和 API
 */
const initCacheAndAPI = (() => {
  // KEY
  const USER_AGENT_KEY = "USER_AGENT_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   {(store: IDBObjectStore) => IDBRequest<IDBCursor | null>} range       清除范围
     * @param   {IDBTransaction}                                          transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async clear(name, range = null, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction, "readwrite");

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

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

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

        return;
      }

      // 请求范围
      const request = range(store);

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

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

          cursor.continue();
        }
      };
    }

    /**
     * 插入指定表的数据
     * @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) {
        // 清空数据
        await this.clear(name, null, transaction);

        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   {(store: IDBObjectStore) => IDBRequest<IDBCursor | null>} range 清除范围
     * @returns {Promise}
     */
    async clear(name, range = null) {
      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.clear(name, range);
      }

      // 清除全部数据
      GM_setValue(name, {});
    }

    /**
     * 插入指定表的数据
     * @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) {
        await this.clear(name, null);

        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 {
    /**
     * 清除指定表的数据
     * @param   {String}  name       表名
     * @param   {Boolean} onlyExpire 是否只清除超时数据
     * @returns {Promise}
     */
    async clear(name, onlyExpire = false) {
      // 如果不在模块里,直接清除
      if (Object.hasOwn(modules, name) === false) {
        return super.clear(name);
      }

      // 如果只清除超时数据为否,直接清除
      if (onlyExpire === false) {
        return super.clear(name);
      }

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

      // 持久化
      if (persistent) {
        return;
      }

      // 清除超时数据
      return super.clear(name, (store) =>
        store
          .index("timestamp")
          .openKeyCursor(IDBKeyRange.upperBound(Date.now() - expireTime))
      );
    }

    /**
     * 插入指定表的数据,并增加 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;

    /**
     * 队列
     */
    queue;

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

    /**
     * 简单的统一请求
     * @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 { posts } = await this.getUserInfo(uid);

      // 发帖数量不准,且可能错误的返回 0
      const value = posts || 0;

      // 发帖数量在泥潭其他接口里命名为 postnum
      const postnum = (() => {
        if (value > 0) {
          return value;
        }

        if (cache) {
          return cache.postnum || 0;
        }

        return 0;
      })();

      // 当发帖数量发生变化时,再重新请求数据
      const needRequest = (() => {
        if (value > 0 && cache) {
          return cache.postnum !== value;
        }

        return true;
      })();

      // 由于泥潭接口限制,同步使用队列请求数据
      if (needRequest) {
        this.queue.enqueue(
          uid,
          () =>
            new Promise(async (resolve, reject) => {
              const result = await this.request(api);

              // 服务器可能返回错误,遇到这种情况下,需要保留缓存
              if (result.data && Number.isInteger(result.data.__ROWS)) {
                this.cache.put(name, {
                  uid,
                  count: result.data.__ROWS,
                  postnum,
                });

                resolve();
                return;
              }

              reject();
            })
        );
      }

      // 直接返回缓存结果
      const count = cache ? cache.count : 0;

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

      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) =>
    Promise.all(Object.keys(modules).map((name) => cache.clear(name, true)));

  // 初始化事件
  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,
    };
  };
})();