Bu script direkt olarak kurulamaz. Başka scriptler için bir kütüphanedir ve meta yönergeleri içerir // @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,
};
};
})();