chore: 补全基础框架

This commit is contained in:
手瓜一十雪 2024-08-08 14:36:59 +08:00
parent e2a6e3ea58
commit 5ae9be0291
5 changed files with 633 additions and 2 deletions

View File

@ -0,0 +1,57 @@
import path from 'node:path';
import fs from 'node:fs';
import { systemPlatform } from '@/common/utils/system';
import { getDefaultQQVersionConfigInfo, getQQVersionConfigPath } from './helper';
import AppidTable from '@/core/external/appid.json';
import { log } from './log';
//基础目录获取
export const QQMainPath = process.execPath;
export const QQPackageInfoPath: string = path.join(path.dirname(QQMainPath), 'resources', 'app', 'package.json');
export const QQVersionConfigPath: string | undefined = getQQVersionConfigPath(QQMainPath);
//基础信息获取 无快更则启用默认模板填充
export const isQuickUpdate: boolean = !!QQVersionConfigPath;
export const QQVersionConfig: QQVersionConfigType = isQuickUpdate ? JSON.parse(fs.readFileSync(QQVersionConfigPath!).toString()) : getDefaultQQVersionConfigInfo();
export const QQPackageInfo: QQPackageInfoType = JSON.parse(fs.readFileSync(QQPackageInfoPath).toString());
export const { appid: QQVersionAppid, qua: QQVersionQua } = getAppidV2();
//基础函数
export function getQQBuildStr() {
return isQuickUpdate ? QQVersionConfig.buildId : QQPackageInfo.buildVersion;
}
export function getFullQQVesion() {
return isQuickUpdate ? QQVersionConfig.curVersion : QQPackageInfo.version;
}
export function requireMinNTQQBuild(buildStr: string) {
return parseInt(getQQBuildStr()) >= parseInt(buildStr);
}
//此方法不要直接使用
export function getQUAInternal() {
return systemPlatform === 'linux' ? `V1_LNX_NQ_${getFullQQVesion()}_${getQQBuildStr()}_GW_B` : `V1_WIN_NQ_${getFullQQVesion()}_${getQQBuildStr()}_GW_B`;
}
export function getAppidV2(): { appid: string, qua: string } {
const appidTbale = AppidTable as unknown as QQAppidTableType;
try {
const data = appidTbale[getFullQQVesion()];
if (data) {
return data;
}
}
catch (e) {
log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
}
// 以下是兜底措施
log(`[QQ版本兼容性检测] ${getFullQQVesion()} 版本兼容性不佳,可能会导致一些功能无法正常使用`);
return { appid: systemPlatform === 'linux' ? '537237950' : '537237765', qua: getQUAInternal() };
}
// platform_type: 3,
// app_type: 4,
// app_version: '9.9.12-25765',
// qua: 'V1_WIN_NQ_9.9.12_25765_GW_B',
// appid: '537234702',
// platVer: '10.0.26100',
// clientVer: '9.9.9-25765',
// Linux
// app_version: '3.2.9-25765',
// qua: 'V1_LNX_NQ_3.2.10_25765_GW_B',

405
src/common/utils/helper.ts Normal file
View File

@ -0,0 +1,405 @@
import crypto from 'node:crypto';
import path from 'node:path';
import fs from 'fs';
import { log, logDebug } from './log';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as fsPromise from 'node:fs/promises';
import os from 'node:os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
//下面这个类是用于将uid+msgid合并的类
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr);
const low = BigInt(lowStr);
const highHex = high.toString(16).padStart(16, '0');
const lowHex = low.toString(16).padStart(16, '0');
const combinedHex = highHex + lowHex;
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(12, 16)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`;
return uuid;
}
static decode(uuid: string): { high: string, low: string } {
const hex = uuid.replace(/-/g, '');
const high = BigInt('0x' + hex.substring(0, 16));
const low = BigInt('0x' + hex.substring(16));
return { high: high.toString(), low: low.toString() };
}
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function PromiseTimer<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeoutPromise = new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('PromiseTimer: Operation timed out')), ms)
);
return Promise.race([promise, timeoutPromise]);
}
export async function runAllWithTimeout<T>(tasks: Promise<T>[], timeout: number): Promise<T[]> {
const wrappedTasks = tasks.map(task =>
PromiseTimer(task, timeout).then(
result => ({ status: 'fulfilled', value: result }),
error => ({ status: 'rejected', reason: error })
)
);
const results = await Promise.all(wrappedTasks);
return results
.filter(result => result.status === 'fulfilled')
.map(result => (result as { status: 'fulfilled'; value: T }).value);
}
export function getMd5(s: string) {
const h = crypto.createHash('md5');
h.update(s);
return h.digest('hex');
}
export function isNull(value: any) {
return value === undefined || value === null;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function simpleDecorator(target: any, context: any) {
}
// export function CacheClassFunc(ttl: number = 3600 * 1000, customKey: string = '') {
// const cache = new Map<string, { expiry: number; value: any }>();
// return function CacheClassFuncDecorator(originalMethod: Function, context: ClassMethodDecoratorContext) {
// async function CacheClassFuncDecoratorInternal(this: any, ...args: any[]) {
// const key = `${customKey}${String(context.name)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`;
// const cachedValue = cache.get(key);
// if (cachedValue && cachedValue.expiry > Date.now()) {
// return cachedValue.value;
// }
// const result = originalMethod.call(this, ...args);
// cache.set(key, { expiry: Date.now() + ttl, value: result });
// return result;
// }
// return CacheClassFuncDecoratorInternal;
// }
// }
export function CacheClassFuncAsync(ttl: number = 3600 * 1000, customKey: string = '') {
//console.log('CacheClassFuncAsync', ttl, customKey);
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
//console.log('logExecutionTime', target, methodName, descriptor);
const cache = new Map<string, { expiry: number; value: any }>();
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`;
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key);
}
});
const cachedValue = cache.get(key);
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value;
}
// const start = Date.now();
const result = await originalMethod.apply(this, args);
// const end = Date.now();
// console.log(`Method ${methodName} executed in ${end - start} ms.`);
cache.set(key, { expiry: Date.now() + ttl, value: result });
return result;
};
}
return logExecutionTime;
}
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true; }) {
//console.log('CacheClassFuncAsync', ttl, customKey);
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
//console.log('logExecutionTime', target, methodName, descriptor);
const cache = new Map<string, { expiry: number; value: any }>();
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`;
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key);
}
});
const cachedValue = cache.get(key);
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value;
}
// const start = Date.now();
const result = await originalMethod.apply(this, args);
if (!checker(...args, result)) {
return result;//丢弃缓存
}
// const end = Date.now();
// console.log(`Method ${methodName} executed in ${end - start} ms.`);
cache.set(key, { expiry: Date.now() + ttl, value: result });
return result;
};
}
return logExecutionTime;
}
// export function CacheClassFuncAsync(ttl: number = 3600 * 1000, customKey: string = ''): any {
// const cache = new Map<string, { expiry: number; value: any }>();
// // 注意在JavaScript装饰器中我们通常不直接处理ClassMethodDecoratorContext这样的类型
// // 因为装饰器的参数通常是目标类(对于类装饰器)、属性名(对于属性装饰器)等。
// // 对于方法装饰器,我们关注的是方法本身及其描述符。
// // 但这里我们维持原逻辑,假设有一个自定义的处理上下文的方式。
// return function (originalMethod: Function): any {
// console.log(originalMethod);
// // 由于JavaScript装饰器原生不支持异步直接定义我们保持async定义以便处理异步方法。
// async function decoratorWrapper(this: any, ...args: any[]): Promise<any> {
// console.log(...args);
// const key = `${customKey}${originalMethod.name}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`;
// const cachedValue = cache.get(key);
// // 遍历cache 清除expiry内容
// cache.forEach((value, key) => {
// if (value.expiry < Date.now()) {
// cache.delete(key);
// }
// });
// if (cachedValue && cachedValue.expiry > Date.now()) {
// return cachedValue.value;
// }
// // 直接await异步方法的结果
// const result = await originalMethod.apply(this, args);
// cache.set(key, { expiry: Date.now() + ttl, value: result });
// return result;
// }
// // 返回装饰后的方法保持与原方法相同的名称和描述符如果需要更精细的控制可以考虑使用Object.getOwnPropertyDescriptor等
// return decoratorWrapper;
// };
// }
/**
* key生成缓存键
* @param ttl
* @param customKey
* @returns
*/
export function cacheFunc(ttl: number, customKey: string = '') {
const cache = new Map<string, { expiry: number; value: any }>();
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value;
const className = target.constructor.name; // 获取类名
const methodName = propertyKey; // 获取方法名
descriptor.value = async function (...args: any[]) {
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`;
const cached = cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
return cached.value;
} else {
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, { value: result, expiry: Date.now() + ttl });
return result;
}
};
return descriptor;
};
}
export function isValidOldConfig(config: any) {
if (typeof config !== 'object') {
return false;
}
const requiredKeys = [
'httpHost', 'httpPort', 'httpPostUrls', 'httpSecret',
'wsHost', 'wsPort', 'wsReverseUrls', 'enableHttp',
'enableHttpHeart', 'enableHttpPost', 'enableWs', 'enableWsReverse',
'messagePostFormat', 'reportSelfMessage', 'enableLocalFile2Url',
'debug', 'heartInterval', 'token', 'musicSignUrl'
];
for (const key of requiredKeys) {
if (!(key in config)) {
return false;
}
}
if (!Array.isArray(config.httpPostUrls) || !Array.isArray(config.wsReverseUrls)) {
return false;
}
if (config.httpPostUrls.some((url: any) => typeof url !== 'string')) {
return false;
}
if (config.wsReverseUrls.some((url: any) => typeof url !== 'string')) {
return false;
}
if (typeof config.httpPort !== 'number' || typeof config.wsPort !== 'number' || typeof config.heartInterval !== 'number') {
return false;
}
if (
typeof config.enableHttp !== 'boolean' ||
typeof config.enableHttpHeart !== 'boolean' ||
typeof config.enableHttpPost !== 'boolean' ||
typeof config.enableWs !== 'boolean' ||
typeof config.enableWsReverse !== 'boolean' ||
typeof config.enableLocalFile2Url !== 'boolean' ||
typeof config.reportSelfMessage !== 'boolean'
) {
return false;
}
if (config.messagePostFormat !== 'array' && config.messagePostFormat !== 'string') {
return false;
}
return true;
}
export function migrateConfig(oldConfig: any) {
const newConfig = {
http: {
enable: oldConfig.enableHttp,
host: oldConfig.httpHost,
port: oldConfig.httpPort,
secret: oldConfig.httpSecret,
enableHeart: oldConfig.enableHttpHeart,
enablePost: oldConfig.enableHttpPost,
postUrls: oldConfig.httpPostUrls,
},
ws: {
enable: oldConfig.enableWs,
host: oldConfig.wsHost,
port: oldConfig.wsPort,
},
reverseWs: {
enable: oldConfig.enableWsReverse,
urls: oldConfig.wsReverseUrls,
},
GroupLocalTime: {
Record: false,
RecordList: []
},
debug: oldConfig.debug,
heartInterval: oldConfig.heartInterval,
messagePostFormat: oldConfig.messagePostFormat,
enableLocalFile2Url: oldConfig.enableLocalFile2Url,
musicSignUrl: oldConfig.musicSignUrl,
reportSelfMessage: oldConfig.reportSelfMessage,
token: oldConfig.token,
};
return newConfig;
}
// 升级旧的配置到新的
export async function UpdateConfig() {
const configFiles = await fsPromise.readdir(path.join(__dirname, 'config'));
for (const file of configFiles) {
if (file.match(/^onebot11_\d+.json$/)) {
const CurrentConfig = JSON.parse(await fsPromise.readFile(path.join(__dirname, 'config', file), 'utf8'));
if (isValidOldConfig(CurrentConfig)) {
log('正在迁移旧配置到新配置 File:', file);
const NewConfig = migrateConfig(CurrentConfig);
await fsPromise.writeFile(path.join(__dirname, 'config', file), JSON.stringify(NewConfig, null, 2));
}
}
}
}
export function isEqual(obj1: any, obj2: any) {
if (obj1 === obj2) return true;
if (obj1 == null || obj2 == null) return false;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!isEqual(obj1[key], obj2[key])) return false;
}
return true;
}
export function getDefaultQQVersionConfigInfo(): QQVersionConfigType {
if (os.platform() === 'linux') {
return {
baseVersion: '3.2.12-26702',
curVersion: '3.2.12-26702',
prevVersion: '',
onErrorVersions: [],
buildId: '26702'
};
}
return {
baseVersion: '9.9.15-26702',
curVersion: '9.9.15-26702',
prevVersion: '',
onErrorVersions: [],
buildId: '26702'
};
}
export async function promisePipeline(promises: Promise<any>[], callback: (result: any) => boolean): Promise<void> {
let callbackCalled = false;
for (const promise of promises) {
if (callbackCalled) break;
try {
const result = await promise;
if (!callbackCalled) {
callbackCalled = callback(result);
}
} catch (error) {
console.error('Error in promise pipeline:', error);
}
}
}
export function getQQVersionConfigPath(exePath: string = ''): string | undefined {
let configVersionInfoPath;
if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json');
} else {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './.config/QQ');
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
}
if (typeof configVersionInfoPath !== 'string') {
return undefined;
}
if (!fs.existsSync(configVersionInfoPath)) {
return undefined;
}
return configVersionInfoPath;
}
export async function deleteOldFiles(directoryPath: string, daysThreshold: number) {
try {
const files = await fsPromise.readdir(directoryPath);
for (const file of files) {
const filePath = path.join(directoryPath, file);
const stats = await fsPromise.stat(filePath);
const lastModifiedTime = stats.mtimeMs;
const currentTime = Date.now();
const timeDifference = currentTime - lastModifiedTime;
const daysDifference = timeDifference / (1000 * 60 * 60 * 24);
if (daysDifference > daysThreshold) {
await fsPromise.unlink(filePath); // Delete the file
//console.log(`Deleted: ${filePath}`);
}
}
} catch (error) {
//console.error('Error deleting files:', error);
}
}

137
src/common/utils/log.ts Normal file
View File

@ -0,0 +1,137 @@
import log4js, { Configuration } from 'log4js';
import { truncateString } from '@/common/utils/helper';
import path from 'node:path';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import chalk from 'chalk';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
FATAL = 'fatal',
}
const logDir = path.join(path.resolve(__dirname), 'logs');
function getFormattedTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${milliseconds}`;
}
const filename = `${getFormattedTimestamp()}.log`;
const logPath = path.join(logDir, filename);
const logConfig: Configuration = {
appenders: {
FileAppender: { // 输出到文件的appender
type: 'file',
filename: logPath, // 指定日志文件的位置和文件名
maxLogSize: 10485760, // 日志文件的最大大小单位字节这里设置为10MB
layout: {
type: 'pattern',
pattern: '%d{yyyy-MM-dd hh:mm:ss} [%p] %X{userInfo} | %m'
}
},
ConsoleAppender: { // 输出到控制台的appender
type: 'console',
layout: {
type: 'pattern',
pattern: `%d{yyyy-MM-dd hh:mm:ss} [%[%p%]] ${chalk.magenta('%X{userInfo}')} | %m`
}
}
},
categories: {
default: { appenders: ['FileAppender', 'ConsoleAppender'], level: 'debug' }, // 默认情况下同时输出到文件和控制台
file: { appenders: ['FileAppender'], level: 'debug' },
console: { appenders: ['ConsoleAppender'], level: 'debug' }
}
};
log4js.configure(logConfig);
const loggerConsole = log4js.getLogger('console');
const loggerFile = log4js.getLogger('file');
const loggerDefault = log4js.getLogger('default');
export function setLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) {
logConfig.categories.file.level = fileLogLevel;
logConfig.categories.console.level = consoleLogLevel;
log4js.configure(logConfig);
}
export function setLogSelfInfo(selfInfo: { nick: string, uin: string, uid: string }) {
const userInfo = `${selfInfo.nick}(${selfInfo.uin})`;
loggerConsole.addContext('userInfo', userInfo);
loggerFile.addContext('userInfo', userInfo);
loggerDefault.addContext('userInfo', userInfo);
}
setLogSelfInfo({ nick: '', uin: '', uid: '' });
let fileLogEnabled = true;
let consoleLogEnabled = true;
export function enableFileLog(enable: boolean) {
fileLogEnabled = enable;
}
export function enableConsoleLog(enable: boolean) {
consoleLogEnabled = enable;
}
function formatMsg(msg: any[]) {
let logMsg = '';
for (const msgItem of msg) {
if (msgItem instanceof Error) { // 判断是否是错误
logMsg += msgItem.stack + ' ';
continue;
} else if (typeof msgItem === 'object') { // 判断是否是对象
const obj = JSON.parse(JSON.stringify(msgItem, null, 2));
logMsg += JSON.stringify(truncateString(obj)) + ' ';
continue;
}
logMsg += msgItem + ' ';
}
return logMsg;
}
// eslint-disable-next-line no-control-regex
const colorEscape = /\x1B[@-_][0-?]*[ -/]*[@-~]/g;
function _log(level: LogLevel, ...args: any[]) {
if (consoleLogEnabled) {
loggerConsole[level](formatMsg(args));
}
if (fileLogEnabled) {
loggerFile[level](formatMsg(args).replace(colorEscape, ''));
}
}
export function log(...args: any[]) {
// info 等级
_log(LogLevel.INFO, ...args);
}
export function logDebug(...args: any[]) {
_log(LogLevel.DEBUG, ...args);
}
export function logError(...args: any[]) {
_log(LogLevel.ERROR, ...args);
}
export function logWarn(...args: any[]) {
_log(LogLevel.WARN, ...args);
}
export function logFatal(...args: any[]) {
_log(LogLevel.FATAL, ...args);
}

17
src/common/utils/type.ts Normal file
View File

@ -0,0 +1,17 @@
//QQVersionType
type QQPackageInfoType = {
version: string;
buildVersion: string;
platform: string;
eleArch: string;
}
type QQVersionConfigType = {
baseVersion: string;
curVersion: string;
prevVersion: string;
onErrorVersions: Array<any>;
buildId: string;
}
type QQAppidTableType = {
[key: string]: { appid: string, qua: string };
}

View File

@ -1,3 +1,5 @@
import { logDebug } from "@/common/utils/log";
import { NodeIKernelLoginService } from "./services";
import { NodeIQQNTWrapperSession } from "./wrapper/wrapper";
export enum NCoreWorkMode {
@ -9,6 +11,19 @@ export class NapCatCore {
public WorkMode: NCoreWorkMode = NCoreWorkMode.Unknown;
public isInit: boolean = false;
public session: NodeIQQNTWrapperSession | undefined;
private proxyHandler = {
get(target: any, prop: any, receiver: any) {
// console.log('get', prop, typeof target[prop]);
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: unknown[]) => {
logDebug(`${target.constructor.name} has no method ${prop}`);
};
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver);
}
};
get IsInit(): boolean {
return this.isInit;
}
@ -21,7 +36,7 @@ export class NapCatShell extends NapCatCore {
}
export class NapCatLiteLoader extends NapCatCore {
public WorkMode: NCoreWorkMode = NCoreWorkMode.LiteLoader;
Init(LoginService: any, WrapperSession: any) {
Init(WrapperSession: NodeIQQNTWrapperSession, LoginService: NodeIKernelLoginService) {
}
}