refactor: 优化eslint配置,提升代码质量 (#1341)

* feat: 统一并标准化eslint

* lint: napcat.webui

* lint: napcat.webui

* lint: napcat.core

* build: fix

* lint: napcat.webui

* refactor: 重构eslint

* Update README.md
This commit is contained in:
时瑾
2025-11-03 16:30:45 +08:00
committed by GitHub
parent c9b1d3fafa
commit 2185a884b4
651 changed files with 37981 additions and 37587 deletions

View File

@@ -2,19 +2,19 @@ import { encode } from 'silk-wasm';
import { parentPort } from 'worker_threads';
export interface EncodeArgs {
input: ArrayBufferView | ArrayBuffer
sampleRate: number
input: ArrayBufferView | ArrayBuffer
sampleRate: number
}
export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
parentPort?.on('message', async (taskData: T) => {
try {
let ret = await cb(taskData);
parentPort?.postMessage(ret);
} catch (error: unknown) {
parentPort?.postMessage({ error: (error as Error).message });
}
});
export function recvTask<T> (cb: (taskData: T) => Promise<unknown>) {
parentPort?.on('message', async (taskData: T) => {
try {
const ret = await cb(taskData);
parentPort?.postMessage(ret);
} catch (error: unknown) {
parentPort?.postMessage({ error: (error as Error).message });
}
});
}
recvTask<EncodeArgs>(async ({ input, sampleRate }) => {
return await encode(input, sampleRate);
});
return await encode(input, sampleRate);
});

View File

@@ -10,76 +10,75 @@ import { fileURLToPath } from 'node:url';
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
function getWorkerPath() {
//return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs');
function getWorkerPath () {
// return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs');
}
async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
async function guessDuration (pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
async function handleWavFile(
file: Buffer,
filePath: string,
pcmPath: string
async function handleWavFile (
file: Buffer,
filePath: string,
pcmPath: string
): Promise<{ input: Buffer; sampleRate: number }> {
const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
const result = await FFmpegService.convert(filePath, pcmPath);
return { input: await fsPromise.readFile(pcmPath), sampleRate: result.sampleRate };
}
return { input: file, sampleRate: fmt.sampleRate };
const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
const result = await FFmpegService.convert(filePath, pcmPath);
return { input: await fsPromise.readFile(pcmPath), sampleRate: result.sampleRate };
}
return { input: file, sampleRate: fmt.sampleRate };
}
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
try {
const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) {
logger.log(`语音文件${filePath}需要转换成silk`);
const pcmPath = `${pttPath}.pcm`;
// const { input, sampleRate } = isWav(file) ? await handleWavFile(file, filePath, pcmPath): { input: await FFmpegService.convert(filePath, pcmPath) ? await fsPromise.readFile(pcmPath) : Buffer.alloc(0), sampleRate: 24000 };
let input: Buffer;
let sampleRate: number;
if (isWav(file)) {
const result = await handleWavFile(file, filePath, pcmPath);
input = result.input;
sampleRate = result.sampleRate;
} else {
const result = await FFmpegService.convert(filePath, pcmPath);
input = await fsPromise.readFile(pcmPath);
sampleRate = result.sampleRate;
}
const silk = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { input: input, sampleRate: sampleRate });
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000,
};
} else {
let duration = 0;
try {
duration = getDuration(file) / 1000;
} catch (e: unknown) {
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack);
duration = await guessDuration(filePath, logger);
}
return {
converted: false,
path: filePath,
duration,
};
}
} catch (error: unknown) {
logger.logError('convert silk failed', error);
return {};
export async function encodeSilk (filePath: string, TEMP_DIR: string, logger: LogWrapper) {
try {
const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) {
logger.log(`语音文件${filePath}需要转换成silk`);
const pcmPath = `${pttPath}.pcm`;
// const { input, sampleRate } = isWav(file) ? await handleWavFile(file, filePath, pcmPath): { input: await FFmpegService.convert(filePath, pcmPath) ? await fsPromise.readFile(pcmPath) : Buffer.alloc(0), sampleRate: 24000 };
let input: Buffer;
let sampleRate: number;
if (isWav(file)) {
const result = await handleWavFile(file, filePath, pcmPath);
input = result.input;
sampleRate = result.sampleRate;
} else {
const result = await FFmpegService.convert(filePath, pcmPath);
input = await fsPromise.readFile(pcmPath);
sampleRate = result.sampleRate;
}
const silk = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { input, sampleRate });
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000,
};
} else {
let duration = 0;
try {
duration = getDuration(file) / 1000;
} catch (e: unknown) {
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack);
duration = await guessDuration(filePath, logger);
}
return {
converted: false,
path: filePath,
duration,
};
}
} catch (error: unknown) {
logger.logError('convert silk failed', error);
return {};
}
}

View File

@@ -1,80 +1,79 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (callback: () => void) => void) => void | Promise<void>;
export class CancelableTask<T> {
private promise: Promise<T>;
private cancelCallback: (() => void) | null = null;
private isCanceled = false;
private cancelListeners: Array<() => void> = [];
private promise: Promise<T>;
private cancelCallback: (() => void) | null = null;
private isCanceled = false;
private cancelListeners: Array<() => void> = [];
constructor(executor: TaskExecutor<T>) {
this.promise = new Promise<T>((resolve, reject) => {
const onCancel = (callback: () => void) => {
this.cancelCallback = callback;
};
constructor (executor: TaskExecutor<T>) {
this.promise = new Promise<T>((resolve, reject) => {
const onCancel = (callback: () => void) => {
this.cancelCallback = callback;
};
const execute = async () => {
try {
await executor(
(value) => {
if (!this.isCanceled) {
resolve(value);
}
},
(reason) => {
if (!this.isCanceled) {
reject(reason);
}
},
onCancel
);
} catch (error) {
if (!this.isCanceled) {
reject(error);
}
}
};
execute();
});
}
public cancel() {
if (this.cancelCallback) {
this.cancelCallback();
const execute = async () => {
try {
await executor(
(value) => {
if (!this.isCanceled) {
resolve(value);
}
},
(reason) => {
if (!this.isCanceled) {
reject(reason);
}
},
onCancel
);
} catch (error) {
if (!this.isCanceled) {
reject(error);
}
}
this.isCanceled = true;
this.cancelListeners.forEach(listener => listener());
}
};
public isTaskCanceled(): boolean {
return this.isCanceled;
}
execute();
});
}
public onCancel(listener: () => void) {
this.cancelListeners.push(listener);
public cancel () {
if (this.cancelCallback) {
this.cancelCallback();
}
this.isCanceled = true;
this.cancelListeners.forEach(listener => listener());
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onfulfilled, onrejected);
}
public isTaskCanceled (): boolean {
return this.isCanceled;
}
public catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
): Promise<T | TResult> {
return this.promise.catch(onrejected);
}
public onCancel (listener: () => void) {
this.cancelListeners.push(listener);
}
public finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return this.promise.finally(onfinally);
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onfulfilled, onrejected);
}
[Symbol.asyncIterator]() {
return {
next: () => this.promise.then(value => ({ value, done: true })),
};
}
}
public catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
): Promise<T | TResult> {
return this.promise.catch(onrejected);
}
public finally (onfinally?: (() => void) | undefined | null): Promise<T> {
return this.promise.finally(onfinally);
}
[Symbol.asyncIterator] () {
return {
next: () => this.promise.then(value => ({ value, done: true })),
};
}
}

View File

@@ -2,228 +2,228 @@ import fs from 'fs';
// generate Claude 3.7 Sonet Thinking
interface FileRecord {
filePath: string;
addedTime: number;
retries: number;
filePath: string;
addedTime: number;
retries: number;
}
interface CleanupTask {
fileRecord: FileRecord;
timer: NodeJS.Timeout;
fileRecord: FileRecord;
timer: NodeJS.Timeout;
}
class CleanupQueue {
private tasks: Map<string, CleanupTask> = new Map();
private readonly MAX_RETRIES = 3;
private isProcessing: boolean = false;
private pendingOperations: Array<() => void> = [];
private tasks: Map<string, CleanupTask> = new Map();
private readonly MAX_RETRIES = 3;
private isProcessing: boolean = false;
private pendingOperations: Array<() => void> = [];
/**
/**
* 执行队列中的待处理操作,确保异步安全
*/
private executeNextOperation(): void {
if (this.pendingOperations.length === 0) {
this.isProcessing = false;
return;
}
this.isProcessing = true;
const operation = this.pendingOperations.shift();
operation?.();
// 使用 setImmediate 允许事件循环继续,防止阻塞
setImmediate(() => this.executeNextOperation());
private executeNextOperation (): void {
if (this.pendingOperations.length === 0) {
this.isProcessing = false;
return;
}
/**
this.isProcessing = true;
const operation = this.pendingOperations.shift();
operation?.();
// 使用 setImmediate 允许事件循环继续,防止阻塞
setImmediate(() => this.executeNextOperation());
}
/**
* 安全执行操作,防止竞态条件
* @param operation 要执行的操作
*/
private safeExecute(operation: () => void): void {
this.pendingOperations.push(operation);
if (!this.isProcessing) {
this.executeNextOperation();
}
private safeExecute (operation: () => void): void {
this.pendingOperations.push(operation);
if (!this.isProcessing) {
this.executeNextOperation();
}
}
/**
/**
* 检查文件是否存在
* @param filePath 文件路径
* @returns 文件是否存在
*/
private fileExists(filePath: string): boolean {
try {
return fs.existsSync(filePath);
} catch (error) {
//console.log(`检查文件存在出错: ${filePath}`, error);
return false;
}
private fileExists (filePath: string): boolean {
try {
return fs.existsSync(filePath);
} catch (_error) {
// console.log(`检查文件存在出错: ${filePath}`, error);
return false;
}
}
/**
/**
* 添加文件到清理队列
* @param filePath 文件路径
* @param cleanupDelay 清理延迟时间(毫秒)
*/
addFile(filePath: string, cleanupDelay: number): void {
this.safeExecute(() => {
// 如果文件已在队列中,取消原来的计时器
if (this.tasks.has(filePath)) {
this.cancelCleanup(filePath);
}
addFile (filePath: string, cleanupDelay: number): void {
this.safeExecute(() => {
// 如果文件已在队列中,取消原来的计时器
if (this.tasks.has(filePath)) {
this.cancelCleanup(filePath);
}
// 创建新的文件记录
const fileRecord: FileRecord = {
filePath,
addedTime: Date.now(),
retries: 0
};
// 创建新的文件记录
const fileRecord: FileRecord = {
filePath,
addedTime: Date.now(),
retries: 0,
};
// 设置计时器
const timer = setTimeout(() => {
this.cleanupFile(fileRecord, cleanupDelay);
}, cleanupDelay);
// 设置计时器
const timer = setTimeout(() => {
this.cleanupFile(fileRecord, cleanupDelay);
}, cleanupDelay);
// 添加到任务队列
this.tasks.set(filePath, { fileRecord, timer });
});
}
// 添加到任务队列
this.tasks.set(filePath, { fileRecord, timer });
});
}
/**
/**
* 批量添加文件到清理队列
* @param filePaths 文件路径数组
* @param cleanupDelay 清理延迟时间(毫秒)
*/
addFiles(filePaths: string[], cleanupDelay: number): void {
this.safeExecute(() => {
for (const filePath of filePaths) {
// 内部直接处理,不通过 safeExecute 以保证批量操作的原子性
if (this.tasks.has(filePath)) {
// 取消已有的计时器,但不使用 cancelCleanup 方法以避免重复的安全检查
const existingTask = this.tasks.get(filePath);
if (existingTask) {
clearTimeout(existingTask.timer);
}
}
addFiles (filePaths: string[], cleanupDelay: number): void {
this.safeExecute(() => {
for (const filePath of filePaths) {
// 内部直接处理,不通过 safeExecute 以保证批量操作的原子性
if (this.tasks.has(filePath)) {
// 取消已有的计时器,但不使用 cancelCleanup 方法以避免重复的安全检查
const existingTask = this.tasks.get(filePath);
if (existingTask) {
clearTimeout(existingTask.timer);
}
}
const fileRecord: FileRecord = {
filePath,
addedTime: Date.now(),
retries: 0
};
const fileRecord: FileRecord = {
filePath,
addedTime: Date.now(),
retries: 0,
};
const timer = setTimeout(() => {
this.cleanupFile(fileRecord, cleanupDelay);
}, cleanupDelay);
const timer = setTimeout(() => {
this.cleanupFile(fileRecord, cleanupDelay);
}, cleanupDelay);
this.tasks.set(filePath, { fileRecord, timer });
}
});
}
this.tasks.set(filePath, { fileRecord, timer });
}
});
}
/**
/**
* 清理文件
* @param record 文件记录
* @param delay 延迟时间,用于重试
*/
private cleanupFile(record: FileRecord, delay: number): void {
this.safeExecute(() => {
// 首先检查文件是否存在,不存在则视为清理成功
if (!this.fileExists(record.filePath)) {
//console.log(`文件已不存在,跳过清理: ${record.filePath}`);
this.tasks.delete(record.filePath);
return;
}
private cleanupFile (record: FileRecord, delay: number): void {
this.safeExecute(() => {
// 首先检查文件是否存在,不存在则视为清理成功
if (!this.fileExists(record.filePath)) {
// console.log(`文件已不存在,跳过清理: ${record.filePath}`);
this.tasks.delete(record.filePath);
return;
}
try {
// 尝试删除文件
fs.unlinkSync(record.filePath);
// 删除成功,从队列中移除任务
this.tasks.delete(record.filePath);
} catch (error) {
const err = error as NodeJS.ErrnoException;
try {
// 尝试删除文件
fs.unlinkSync(record.filePath);
// 删除成功,从队列中移除任务
this.tasks.delete(record.filePath);
} catch (error) {
const err = error as NodeJS.ErrnoException;
// 明确处理文件不存在的情况
if (err.code === 'ENOENT') {
//console.log(`文件在删除时不存在,视为清理成功: ${record.filePath}`);
this.tasks.delete(record.filePath);
return;
}
// 明确处理文件不存在的情况
if (err.code === 'ENOENT') {
// console.log(`文件在删除时不存在,视为清理成功: ${record.filePath}`);
this.tasks.delete(record.filePath);
return;
}
// 文件没有访问权限等情况
if (err.code === 'EACCES' || err.code === 'EPERM') {
//console.error(`没有权限删除文件: ${record.filePath}`, err);
}
// 文件没有访问权限等情况
if (err.code === 'EACCES' || err.code === 'EPERM') {
// console.error(`没有权限删除文件: ${record.filePath}`, err);
}
// 其他删除失败情况,考虑重试
if (record.retries < this.MAX_RETRIES - 1) {
// 还有重试机会,增加重试次数
record.retries++;
//console.log(`清理文件失败,将重试(${record.retries}/${this.MAX_RETRIES}): ${record.filePath}`);
// 其他删除失败情况,考虑重试
if (record.retries < this.MAX_RETRIES - 1) {
// 还有重试机会,增加重试次数
record.retries++;
// console.log(`清理文件失败,将重试(${record.retries}/${this.MAX_RETRIES}): ${record.filePath}`);
// 设置相同的延迟时间再次尝试
const timer = setTimeout(() => {
this.cleanupFile(record, delay);
}, delay);
// 设置相同的延迟时间再次尝试
const timer = setTimeout(() => {
this.cleanupFile(record, delay);
}, delay);
// 更新任务
this.tasks.set(record.filePath, { fileRecord: record, timer });
} else {
// 已达到最大重试次数,从队列中移除任务
this.tasks.delete(record.filePath);
//console.error(`清理文件失败,已达最大重试次数(${this.MAX_RETRIES}): ${record.filePath}`, error);
}
}
});
}
// 更新任务
this.tasks.set(record.filePath, { fileRecord: record, timer });
} else {
// 已达到最大重试次数,从队列中移除任务
this.tasks.delete(record.filePath);
// console.error(`清理文件失败,已达最大重试次数(${this.MAX_RETRIES}): ${record.filePath}`, error);
}
}
});
}
/**
/**
* 取消文件的清理任务
* @param filePath 文件路径
* @returns 是否成功取消
*/
cancelCleanup(filePath: string): boolean {
let cancelled = false;
this.safeExecute(() => {
const task = this.tasks.get(filePath);
if (task) {
clearTimeout(task.timer);
this.tasks.delete(filePath);
cancelled = true;
}
});
return cancelled;
}
cancelCleanup (filePath: string): boolean {
let cancelled = false;
this.safeExecute(() => {
const task = this.tasks.get(filePath);
if (task) {
clearTimeout(task.timer);
this.tasks.delete(filePath);
cancelled = true;
}
});
return cancelled;
}
/**
/**
* 获取队列中的文件数量
* @returns 文件数量
*/
getQueueSize(): number {
return this.tasks.size;
}
getQueueSize (): number {
return this.tasks.size;
}
/**
/**
* 获取所有待清理的文件
* @returns 文件路径数组
*/
getPendingFiles(): string[] {
return Array.from(this.tasks.keys());
}
getPendingFiles (): string[] {
return Array.from(this.tasks.keys());
}
/**
/**
* 清空所有清理任务
*/
clearAll(): void {
this.safeExecute(() => {
// 取消所有定时器
for (const task of this.tasks.values()) {
clearTimeout(task.timer);
}
this.tasks.clear();
//console.log('已清空所有清理任务');
});
}
clearAll (): void {
this.safeExecute(() => {
// 取消所有定时器
for (const task of this.tasks.values()) {
clearTimeout(task.timer);
}
this.tasks.clear();
// console.log('已清空所有清理任务');
});
}
}
export const cleanTaskQueue = new CleanupQueue();
export const cleanTaskQueue = new CleanupQueue();

View File

@@ -5,70 +5,70 @@ import json5 from 'json5';
import Ajv, { AnySchema, ValidateFunction } from 'ajv';
export abstract class ConfigBase<T> {
name: string;
core: NapCatCore;
configPath: string;
configData: T = {} as T;
ajv: Ajv;
validate: ValidateFunction<T>;
name: string;
core: NapCatCore;
configPath: string;
configData: T = {} as T;
ajv: Ajv;
validate: ValidateFunction<T>;
protected constructor(name: string, core: NapCatCore, configPath: string, ConfigSchema: AnySchema) {
this.name = name;
this.core = core;
this.configPath = configPath;
this.ajv = new Ajv({ useDefaults: true, coerceTypes: true });
this.validate = this.ajv.compile<T>(ConfigSchema);
fs.mkdirSync(this.configPath, { recursive: true });
this.read();
}
protected constructor (name: string, core: NapCatCore, configPath: string, ConfigSchema: AnySchema) {
this.name = name;
this.core = core;
this.configPath = configPath;
this.ajv = new Ajv({ useDefaults: true, coerceTypes: true });
this.validate = this.ajv.compile<T>(ConfigSchema);
fs.mkdirSync(this.configPath, { recursive: true });
this.read();
}
getConfigPath(pathName?: string): string {
const filename = pathName ? `${this.name}_${pathName}.json` : `${this.name}.json`;
return path.join(this.configPath, filename);
}
getConfigPath (pathName?: string): string {
const filename = pathName ? `${this.name}_${pathName}.json` : `${this.name}.json`;
return path.join(this.configPath, filename);
}
read(): T {
const configPath = this.getConfigPath(this.core.selfInfo.uin);
const defaultConfigPath = this.getConfigPath();
if (!fs.existsSync(configPath)) {
if (fs.existsSync(defaultConfigPath)) {
this.configData = this.loadConfig(defaultConfigPath);
}
this.save();
return this.configData;
}
return this.loadConfig(configPath);
read (): T {
const configPath = this.getConfigPath(this.core.selfInfo.uin);
const defaultConfigPath = this.getConfigPath();
if (!fs.existsSync(configPath)) {
if (fs.existsSync(defaultConfigPath)) {
this.configData = this.loadConfig(defaultConfigPath);
}
this.save();
return this.configData;
}
return this.loadConfig(configPath);
}
private loadConfig(configPath: string): T {
try {
let newConfigData = json5.parse(fs.readFileSync(configPath, 'utf-8'));
this.validate(newConfigData);
this.configData = newConfigData;
this.core.context.logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
return this.configData;
} catch (e: unknown) {
this.handleError(e, '读取配置文件时发生错误');
return {} as T;
}
private loadConfig (configPath: string): T {
try {
const newConfigData = json5.parse(fs.readFileSync(configPath, 'utf-8'));
this.validate(newConfigData);
this.configData = newConfigData;
this.core.context.logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
return this.configData;
} catch (e: unknown) {
this.handleError(e, '读取配置文件时发生错误');
return {} as T;
}
}
save(newConfigData: T = this.configData): void {
const configPath = this.getConfigPath(this.core.selfInfo.uin);
this.validate(newConfigData);
this.configData = newConfigData;
try {
fs.writeFileSync(configPath, JSON.stringify(this.configData, null, 2));
} catch (e: unknown) {
this.handleError(e, `保存配置文件 ${configPath} 时发生错误:`);
}
save (newConfigData: T = this.configData): void {
const configPath = this.getConfigPath(this.core.selfInfo.uin);
this.validate(newConfigData);
this.configData = newConfigData;
try {
fs.writeFileSync(configPath, JSON.stringify(this.configData, null, 2));
} catch (e: unknown) {
this.handleError(e, `保存配置文件 ${configPath} 时发生错误:`);
}
}
private handleError(e: unknown, message: string): void {
if (e instanceof SyntaxError) {
this.core.context.logger.logError('[Core] [Config] 操作配置文件格式错误,请检查配置文件:', e.message);
} else {
this.core.context.logger.logError(`[Core] [Config] ${message}:`, (e as Error).message);
}
private handleError (e: unknown, message: string): void {
if (e instanceof SyntaxError) {
this.core.context.logger.logError('[Core] [Config] 操作配置文件格式错误,请检查配置文件:', e.message);
} else {
this.core.context.logger.logError(`[Core] [Config] ${message}:`, (e as Error).message);
}
}
}
}

View File

@@ -8,17 +8,17 @@ import { pipeline } from 'stream/promises';
import { fileURLToPath } from 'url';
import { LogWrapper } from './log';
const downloadOri = "https://github.com/NapNeko/ffmpeg-build/releases/download/v1.0.0/ffmpeg-7.1.1-win64.zip"
const downloadOri = 'https://github.com/NapNeko/ffmpeg-build/releases/download/v1.0.0/ffmpeg-7.1.1-win64.zip';
const urls = [
"https://j.1win.ggff.net/" + downloadOri,
"https://git.yylx.win/" + downloadOri,
"https://ghfile.geekertao.top/" + downloadOri,
"https://gh-proxy.net/" + downloadOri,
"https://ghm.078465.xyz/" + downloadOri,
"https://gitproxy.127731.xyz/" + downloadOri,
"https://jiashu.1win.eu.org/" + downloadOri,
"https://github.tbedu.top/" + downloadOri,
downloadOri
'https://j.1win.ggff.net/' + downloadOri,
'https://git.yylx.win/' + downloadOri,
'https://ghfile.geekertao.top/' + downloadOri,
'https://gh-proxy.net/' + downloadOri,
'https://ghm.078465.xyz/' + downloadOri,
'https://gitproxy.127731.xyz/' + downloadOri,
'https://jiashu.1win.eu.org/' + downloadOri,
'https://github.tbedu.top/' + downloadOri,
downloadOri,
];
/**
@@ -26,49 +26,49 @@ const urls = [
* @param url 待测试的URL
* @returns 如果URL可访问返回true否则返回false
*/
async function testUrl(url: string): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const req = https.get(url, { timeout: 5000 }, (res) => {
// 检查状态码是否表示成功
const statusCode = res.statusCode || 0;
if (statusCode >= 200 && statusCode < 300) {
// 终止请求并返回true
req.destroy();
resolve(true);
} else {
req.destroy();
resolve(false);
}
});
req.on('error', () => {
resolve(false);
});
req.on('timeout', () => {
req.destroy();
resolve(false);
});
async function testUrl (url: string): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const req = https.get(url, { timeout: 5000 }, (res) => {
// 检查状态码是否表示成功
const statusCode = res.statusCode || 0;
if (statusCode >= 200 && statusCode < 300) {
// 终止请求并返回true
req.destroy();
resolve(true);
} else {
req.destroy();
resolve(false);
}
});
req.on('error', () => {
resolve(false);
});
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
/**
* 查找第一个可用的URL
* @returns 返回第一个可用的URL如果都不可用则返回null
*/
async function findAvailableUrl(): Promise<string | null> {
for (const url of urls) {
try {
const available = await testUrl(url);
if (available) {
return url;
}
} catch (error) {
// 忽略错误
}
async function findAvailableUrl (): Promise<string | null> {
for (const url of urls) {
try {
const available = await testUrl(url);
if (available) {
return url;
}
} catch (_error) {
// 忽略错误
}
}
return null;
return null;
}
/**
* 下载文件
@@ -76,68 +76,67 @@ async function findAvailableUrl(): Promise<string | null> {
* @param destPath 目标保存路径
* @returns 成功返回true失败返回false
*/
async function downloadFile(url: string, destPath: string, progressCallback?: (percent: number) => void): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const file = fs.createWriteStream(destPath);
async function downloadFile (url: string, destPath: string, progressCallback?: (percent: number) => void): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const file = fs.createWriteStream(destPath);
const req = https.get(url, (res) => {
const statusCode = res.statusCode || 0;
const req = https.get(url, (res) => {
const statusCode = res.statusCode || 0;
if (statusCode >= 200 && statusCode < 300) {
// 获取文件总大小
const totalSize = parseInt(res.headers['content-length'] || '0', 10);
let downloadedSize = 0;
let lastReportedPercent = -1; // 上次报告的百分比
let lastReportTime = 0; // 上次报告的时间戳
if (statusCode >= 200 && statusCode < 300) {
// 获取文件总大小
const totalSize = parseInt(res.headers['content-length'] || '0', 10);
let downloadedSize = 0;
let lastReportedPercent = -1; // 上次报告的百分比
let lastReportTime = 0; // 上次报告的时间戳
// 如果有内容长度和进度回调,则添加数据监听
if (totalSize > 0 && progressCallback) {
// 初始报告 0%
progressCallback(0);
lastReportTime = Date.now();
// 如果有内容长度和进度回调,则添加数据监听
if (totalSize > 0 && progressCallback) {
// 初始报告 0%
progressCallback(0);
lastReportTime = Date.now();
res.on('data', (chunk) => {
downloadedSize += chunk.length;
const currentPercent = Math.floor((downloadedSize / totalSize) * 100);
const now = Date.now();
res.on('data', (chunk) => {
downloadedSize += chunk.length;
const currentPercent = Math.floor((downloadedSize / totalSize) * 100);
const now = Date.now();
// 只在以下条件触发回调:
// 1. 百分比变化至少为1%
// 2. 距离上次报告至少500毫秒
// 3. 确保报告100%完成
if ((currentPercent !== lastReportedPercent &&
(currentPercent - lastReportedPercent >= 1 || currentPercent === 100)) &&
(now - lastReportTime >= 1000 || currentPercent === 100)) {
progressCallback(currentPercent);
lastReportedPercent = currentPercent;
lastReportTime = now;
}
});
}
pipeline(res, file)
.then(() => {
// 确保最后报告100%
if (progressCallback && lastReportedPercent !== 100) {
progressCallback(100);
}
resolve(true);
})
.catch(() => resolve(false));
} else {
file.close();
fs.unlink(destPath, () => { });
resolve(false);
// 只在以下条件触发回调:
// 1. 百分比变化至少为1%
// 2. 距离上次报告至少500毫秒
// 3. 确保报告100%完成
if ((currentPercent !== lastReportedPercent &&
(currentPercent - lastReportedPercent >= 1 || currentPercent === 100)) &&
(now - lastReportTime >= 1000 || currentPercent === 100)) {
progressCallback(currentPercent);
lastReportedPercent = currentPercent;
lastReportTime = now;
}
});
});
}
req.on('error', () => {
file.close();
fs.unlink(destPath, () => { });
resolve(false);
});
pipeline(res, file)
.then(() => {
// 确保最后报告100%
if (progressCallback && lastReportedPercent !== 100) {
progressCallback(100);
}
resolve(true);
})
.catch(() => resolve(false));
} else {
file.close();
fs.unlink(destPath, () => { });
resolve(false);
}
});
req.on('error', () => {
file.close();
fs.unlink(destPath, () => { });
resolve(false);
});
});
}
/**
@@ -146,61 +145,57 @@ async function downloadFile(url: string, destPath: string, progressCallback?: (p
* @param zipPath 压缩文件路径
* @param extractDir 解压目标路径
*/
async function extractBinDirectory(zipPath: string, extractDir: string): Promise<void> {
try {
// 确保目标目录存在
if (!fs.existsSync(extractDir)) {
fs.mkdirSync(extractDir, { recursive: true });
}
async function extractBinDirectory (zipPath: string, extractDir: string): Promise<void> {
// 确保目标目录存在
if (!fs.existsSync(extractDir)) {
fs.mkdirSync(extractDir, { recursive: true });
}
// 解压文件
const zipStream = new compressing.zip.UncompressStream({ source: zipPath });
// 解压文件
const zipStream = new compressing.zip.UncompressStream({ source: zipPath });
return new Promise<void>((resolve, reject) => {
// 监听条目事件
zipStream.on('entry', (header, stream, next) => {
// 获取文件路径
const filePath = header.name;
return new Promise<void>((resolve, reject) => {
// 监听条目事件
zipStream.on('entry', (header, stream, next) => {
// 获取文件路径
const filePath = header.name;
// 匹配内层bin目录中的文件
// 例如ffmpeg-n7.1.1-6-g48c0f071d4-win64-lgpl-7.1/bin/ffmpeg.exe
if (filePath.includes('/bin/') && filePath.endsWith('.exe')) {
// 提取文件名
const fileName = path.basename(filePath);
const targetPath = path.join(extractDir, fileName);
// 匹配内层bin目录中的文件
// 例如ffmpeg-n7.1.1-6-g48c0f071d4-win64-lgpl-7.1/bin/ffmpeg.exe
if (filePath.includes('/bin/') && filePath.endsWith('.exe')) {
// 提取文件名
const fileName = path.basename(filePath);
const targetPath = path.join(extractDir, fileName);
// 创建写入流
const writeStream = fs.createWriteStream(targetPath);
// 创建写入流
const writeStream = fs.createWriteStream(targetPath);
// 将流管道连接到文件
stream.pipe(writeStream);
// 将流管道连接到文件
stream.pipe(writeStream);
// 监听写入完成事件
writeStream.on('finish', () => {
next();
});
writeStream.on('error', () => {
next();
});
} else {
// 跳过不需要的文件
stream.resume();
next();
}
});
zipStream.on('error', (err) => {
reject(err);
});
zipStream.on('finish', () => {
resolve();
});
// 监听写入完成事件
writeStream.on('finish', () => {
next();
});
} catch (err) {
throw err;
}
writeStream.on('error', () => {
next();
});
} else {
// 跳过不需要的文件
stream.resume();
next();
}
});
zipStream.on('error', (err) => {
reject(err);
});
zipStream.on('finish', () => {
resolve();
});
});
}
/**
@@ -209,82 +204,82 @@ async function extractBinDirectory(zipPath: string, extractDir: string): Promise
* @param tempDir 临时文件目录,默认为系统临时目录
* @returns 返回ffmpeg可执行文件的路径如果失败则返回null
*/
export async function downloadFFmpeg(
destDir?: string,
tempDir?: string,
progressCallback?: (percent: number, stage: string) => void
export async function downloadFFmpeg (
destDir?: string,
tempDir?: string,
progressCallback?: (percent: number, stage: string) => void
): Promise<string | null> {
// 仅限Windows
if (os.platform() !== 'win32') {
return null;
// 仅限Windows
if (os.platform() !== 'win32') {
return null;
}
const destinationDir = destDir || path.join(os.tmpdir(), 'ffmpeg');
const tempDirectory = tempDir || os.tmpdir();
const zipFilePath = path.join(tempDirectory, 'ffmpeg.zip'); // 临时下载到指定临时目录
const ffmpegExePath = path.join(destinationDir, 'ffmpeg.exe');
// 确保目录存在
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir, { recursive: true });
}
// 确保临时目录存在
if (!fs.existsSync(tempDirectory)) {
fs.mkdirSync(tempDirectory, { recursive: true });
}
// 如果ffmpeg已经存在直接返回路径
if (fs.existsSync(ffmpegExePath)) {
if (progressCallback) progressCallback(100, '已找到FFmpeg');
return ffmpegExePath;
}
// 查找可用URL
if (progressCallback) progressCallback(0, '查找可用下载源');
const availableUrl = await findAvailableUrl();
if (!availableUrl) {
return null;
}
// 下载文件
if (progressCallback) progressCallback(5, '开始下载FFmpeg');
const downloaded = await downloadFile(
availableUrl,
zipFilePath,
(percent) => {
// 下载占总进度的70%
if (progressCallback) progressCallback(5 + Math.floor(percent * 0.7), '下载FFmpeg');
}
);
const destinationDir = destDir || path.join(os.tmpdir(), 'ffmpeg');
const tempDirectory = tempDir || os.tmpdir();
const zipFilePath = path.join(tempDirectory, 'ffmpeg.zip'); // 临时下载到指定临时目录
const ffmpegExePath = path.join(destinationDir, 'ffmpeg.exe');
if (!downloaded) {
return null;
}
// 确保目录存在
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir, { recursive: true });
}
// 确保临时目录存在
if (!fs.existsSync(tempDirectory)) {
fs.mkdirSync(tempDirectory, { recursive: true });
}
// 如果ffmpeg已经存在直接返回路径
if (fs.existsSync(ffmpegExePath)) {
if (progressCallback) progressCallback(100, '已找到FFmpeg');
return ffmpegExePath;
}
// 查找可用URL
if (progressCallback) progressCallback(0, '查找可用下载源');
const availableUrl = await findAvailableUrl();
if (!availableUrl) {
return null;
}
// 下载文件
if (progressCallback) progressCallback(5, '开始下载FFmpeg');
const downloaded = await downloadFile(
availableUrl,
zipFilePath,
(percent) => {
// 下载占总进度的70%
if (progressCallback) progressCallback(5 + Math.floor(percent * 0.7), '下载FFmpeg');
}
);
if (!downloaded) {
return null;
}
try {
// 直接解压bin目录文件到目标目录
if (progressCallback) progressCallback(75, '解压FFmpeg');
await extractBinDirectory(zipFilePath, destinationDir);
// 清理下载文件
if (progressCallback) progressCallback(95, '清理临时文件');
try {
// 直接解压bin目录文件到目标目录
if (progressCallback) progressCallback(75, '解压FFmpeg');
await extractBinDirectory(zipFilePath, destinationDir);
// 清理下载文件
if (progressCallback) progressCallback(95, '清理临时文件');
try {
fs.unlinkSync(zipFilePath);
} catch (err) {
// 忽略清理临时文件失败的错误
}
// 检查ffmpeg.exe是否成功解压
if (fs.existsSync(ffmpegExePath)) {
if (progressCallback) progressCallback(100, 'FFmpeg安装完成');
return ffmpegExePath;
} else {
return null;
}
} catch (err) {
return null;
fs.unlinkSync(zipFilePath);
} catch (_err) {
// 忽略清理临时文件失败的错误
}
// 检查ffmpeg.exe是否成功解压
if (fs.existsSync(ffmpegExePath)) {
if (progressCallback) progressCallback(100, 'FFmpeg安装完成');
return ffmpegExePath;
} else {
return null;
}
} catch (_err) {
return null;
}
}
/**
@@ -292,73 +287,73 @@ export async function downloadFFmpeg(
* @param executable 可执行文件名
* @returns 如果找到返回完整路径否则返回null
*/
function findExecutableInPath(executable: string): string | null {
// 仅适用于Windows系统
if (os.platform() !== 'win32') return null;
function findExecutableInPath (executable: string): string | null {
// 仅适用于Windows系统
if (os.platform() !== 'win32') return null;
// 获取PATH环境变量
const pathEnv = process.env['PATH'] || '';
const pathDirs = pathEnv.split(';');
// 获取PATH环境变量
const pathEnv = process.env['PATH'] || '';
const pathDirs = pathEnv.split(';');
// 检查每个目录
for (const dir of pathDirs) {
if (!dir) continue;
try {
const filePath = path.join(dir, executable);
if (fs.existsSync(filePath)) {
return filePath;
}
} catch (error) {
continue;
}
// 检查每个目录
for (const dir of pathDirs) {
if (!dir) continue;
try {
const filePath = path.join(dir, executable);
if (fs.existsSync(filePath)) {
return filePath;
}
} catch (_error) {
continue;
}
}
return null;
return null;
}
export async function downloadFFmpegIfNotExists(log: LogWrapper) {
// 仅限Windows
if (os.platform() !== 'win32') {
return {
path: null,
reset: false
};
}
const ffmpegInPath = findExecutableInPath('ffmpeg.exe');
const ffprobeInPath = findExecutableInPath('ffprobe.exe');
if (ffmpegInPath && ffprobeInPath) {
const ffmpegDir = path.dirname(ffmpegInPath);
return {
path: ffmpegDir,
reset: true
};
}
// 如果环境变量中没有,检查项目目录中是否存在
const currentPath = path.dirname(fileURLToPath(import.meta.url));
const ffmpeg_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffmpeg.exe'));
const ffprobe_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffprobe.exe'));
if (!ffmpeg_exist || !ffprobe_exist) {
let url = await downloadFFmpeg(path.join(currentPath, 'ffmpeg'), path.join(currentPath, 'cache'), (percentage: number, message: string) => {
log.log(`[FFmpeg] [Download] ${percentage}% - ${message}`);
});
if (!url) {
log.log('[FFmpeg] [Error] 下载FFmpeg失败');
return {
path: null,
reset: false
};
}
return {
path: path.join(currentPath, 'ffmpeg'),
reset: true
}
}
export async function downloadFFmpegIfNotExists (log: LogWrapper) {
// 仅限Windows
if (os.platform() !== 'win32') {
return {
path: path.join(currentPath, 'ffmpeg'),
reset: true
path: null,
reset: false,
};
}
const ffmpegInPath = findExecutableInPath('ffmpeg.exe');
const ffprobeInPath = findExecutableInPath('ffprobe.exe');
if (ffmpegInPath && ffprobeInPath) {
const ffmpegDir = path.dirname(ffmpegInPath);
return {
path: ffmpegDir,
reset: true,
};
}
// 如果环境变量中没有,检查项目目录中是否存在
const currentPath = path.dirname(fileURLToPath(import.meta.url));
const ffmpeg_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffmpeg.exe'));
const ffprobe_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffprobe.exe'));
if (!ffmpeg_exist || !ffprobe_exist) {
const url = await downloadFFmpeg(path.join(currentPath, 'ffmpeg'), path.join(currentPath, 'cache'), (percentage: number, message: string) => {
log.log(`[FFmpeg] [Download] ${percentage}% - ${message}`);
});
if (!url) {
log.log('[FFmpeg] [Error] 下载FFmpeg失败');
return {
path: null,
reset: false,
};
}
}
return {
path: path.join(currentPath, 'ffmpeg'),
reset: true,
};
}
return {
path: path.join(currentPath, 'ffmpeg'),
reset: true,
};
}

View File

@@ -1,273 +1,272 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NodeIQQNTWrapperSession } from '@/core/wrapper';
import { randomUUID } from 'crypto';
import { ListenerNamingMapping, ServiceNamingMapping } from '@/core';
interface InternalMapKey {
timeout: number;
createtime: number;
func: (...arg: any[]) => any;
checker: ((...args: any[]) => boolean) | undefined;
timeout: number;
createtime: number;
func: (...arg: any[]) => any;
checker: ((...args: any[]) => boolean) | undefined;
}
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
type FuncKeys<T> = Extract<
{
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
}[keyof T],
string
{
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
}[keyof T],
string
>;
export type ListenerClassBase = Record<string, string>;
export class NTEventWrapper {
private readonly WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
private readonly listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
private readonly EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
private readonly WrapperSession: NodeIQQNTWrapperSession | undefined; // WrapperSession
private readonly listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); // ListenerName-Unique -> Listener实例
private readonly EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); // tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
constructor(
wrapperSession: NodeIQQNTWrapperSession,
) {
this.WrapperSession = wrapperSession;
}
constructor (
wrapperSession: NodeIQQNTWrapperSession
) {
this.WrapperSession = wrapperSession;
}
createProxyDispatch(ListenerMainName: string) {
const dispatcherListenerFunc = this.dispatcherListener.bind(this);
return new Proxy(
{},
{
get(target: any, prop: any, receiver: any) {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => {
dispatcherListenerFunc(ListenerMainName, prop, ...args).then();
};
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver);
},
},
);
}
createEventFunction<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
const eventNameArr = eventName.split('/');
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> };
};
if (eventNameArr.length > 1) {
const serviceName = 'get' + (eventNameArr[0]?.replace('NodeIKernel', '') ?? '');
const eventName = eventNameArr[1];
const services = (this.WrapperSession as unknown as eventType)[serviceName]?.();
if (!services || !eventName) {
return undefined;
}
let event = services[eventName];
//重新绑定this
event = event?.bind(services);
if (event) {
return event as T;
}
return undefined;
}
return undefined;
}
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const existListener = this.listenerManager.get(listenerMainName + uniqueCode);
if (!existListener) {
const Listener = this.createProxyDispatch(listenerMainName);
const ServiceSubName = /^NodeIKernel(.*?)Listener$/.exec(listenerMainName)![1];
const Service = `NodeIKernel${ServiceSubName}Service/addKernel${ServiceSubName}Listener`;
// eslint-disable-next-line
// @ts-ignore
this.createEventFunction(Service)(Listener as T);
this.listenerManager.set(listenerMainName + uniqueCode, Listener);
return Listener as T;
}
return existListener as T;
}
//统一回调清理事件
async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
this.EventTask.get(ListenerMainName)
?.get(ListenerSubName)
?.forEach((task, uuid) => {
if (task.createtime + task.timeout < Date.now()) {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid);
return;
}
if (task?.checker?.(...args)) {
task.func(...args);
}
});
}
async callNoListenerEvent<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
...args: Parameters<EventType>
): Promise<Awaited<ReturnType<EventType>>> {
return (this.createEventFunction(serviceAndMethod))!(...args);
}
async registerListen<
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>,
>(
listenerAndMethod: `${Listener}/${ListenerMethod}`,
checker: (...args: Parameters<ListenerType>) => boolean,
waitTimes = 1,
timeout = 5000,
) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0] ?? '';
const ListenerSubName = ListenerNameList[1] ?? '';
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined;
function sendDataCallback() {
if (complete == 0) {
reject(new Error(' ListenerName:' + listenerAndMethod + ' timeout'));
} else {
resolve(retData!);
}
}
const timeoutRef = setTimeout(sendDataCallback, timeout);
const eventCallback = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: Parameters<ListenerType>) => {
complete++;
retData = args;
if (complete >= waitTimes) {
clearTimeout(timeoutRef);
sendDataCallback();
}
},
createProxyDispatch (ListenerMainName: string) {
const dispatcherListenerFunc = this.dispatcherListener.bind(this);
return new Proxy(
{},
{
get (target: any, prop: any, receiver: any) {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => {
dispatcherListenerFunc(ListenerMainName, prop, ...args).then();
};
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map());
}
if (!this.EventTask.get(ListenerMainName)?.get(ListenerSubName)) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
this.createListenerFunction(ListenerMainName);
});
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver);
},
}
);
}
createEventFunction<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>
> (eventName: `${Service}/${ServiceMethod}`): T | undefined {
const eventNameArr = eventName.split('/');
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>>; };
};
if (eventNameArr.length > 1) {
const serviceName = 'get' + (eventNameArr[0]?.replace('NodeIKernel', '') ?? '');
const eventName = eventNameArr[1];
const services = (this.WrapperSession as unknown as eventType)[serviceName]?.();
if (!services || !eventName) {
return undefined;
}
let event = services[eventName];
// 重新绑定this
event = event?.bind(services);
if (event) {
return event as T;
}
return undefined;
}
return undefined;
}
async callNormalEventV2<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
listenerAndMethod: `${Listener}/${ListenerMethod}`,
args: Parameters<EventType>,
checkerEvent: (ret: Awaited<ReturnType<EventType>>) => boolean = () => true,
checkerListener: (...args: Parameters<ListenerType>) => boolean = () => true,
callbackTimesToWait = 1,
timeout = 5000,
) {
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined;
let retEvent: any = {};
createListenerFunction<T> (listenerMainName: string, uniqueCode: string = ''): T {
const existListener = this.listenerManager.get(listenerMainName + uniqueCode);
if (!existListener) {
const Listener = this.createProxyDispatch(listenerMainName);
const ServiceSubName = /^NodeIKernel(.*?)Listener$/.exec(listenerMainName)![1];
const Service = `NodeIKernel${ServiceSubName}Service/addKernel${ServiceSubName}Listener`;
function sendDataCallback(resolve: any, reject: any) {
if (complete == 0) {
reject(
new Error(
'Timeout: NTEvent serviceAndMethod:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n',
),
);
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
}
// @ts-ignore
this.createEventFunction(Service)(Listener as T);
this.listenerManager.set(listenerMainName + uniqueCode, Listener);
return Listener as T;
}
return existListener as T;
}
// 统一回调清理事件
async dispatcherListener (ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
this.EventTask.get(ListenerMainName)
?.get(ListenerSubName)
?.forEach((task, uuid) => {
if (task.createtime + task.timeout < Date.now()) {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid);
return;
}
if (task?.checker?.(...args)) {
task.func(...args);
}
});
}
const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0]??'';
const ListenerSubName = ListenerNameList[1]??'';
async callNoListenerEvent<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>
> (
serviceAndMethod: `${Service}/${ServiceMethod}`,
...args: Parameters<EventType>
): Promise<Awaited<ReturnType<EventType>>> {
return (this.createEventFunction(serviceAndMethod))!(...args);
}
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
(resolve, reject) => {
const timeoutRef = setTimeout(() => sendDataCallback(resolve, reject), timeout);
async registerListen<
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
> (
listenerAndMethod: `${Listener}/${ListenerMethod}`,
checker: (...args: Parameters<ListenerType>) => boolean,
waitTimes = 1,
timeout = 5000
) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0] ?? '';
const ListenerSubName = ListenerNameList[1] ?? '';
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined;
const eventCallback = {
timeout: timeout,
createtime: Date.now(),
checker: checkerListener,
func: (...args: any[]) => {
complete++;
retData = args as Parameters<ListenerType>;
if (complete >= callbackTimesToWait) {
clearTimeout(timeoutRef);
sendDataCallback(resolve, reject);
}
},
};
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map());
}
if (!this.EventTask.get(ListenerMainName)?.get(ListenerSubName)) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
this.createListenerFunction(ListenerMainName);
function sendDataCallback () {
if (complete === 0) {
reject(new Error(' ListenerName:' + listenerAndMethod + ' timeout'));
} else {
resolve(retData!);
}
}
const eventResult = this.createEventFunction(serviceAndMethod)!(...(args));
const timeoutRef = setTimeout(sendDataCallback, timeout);
const eventCallback = {
timeout,
createtime: Date.now(),
checker,
func: (...args: Parameters<ListenerType>) => {
complete++;
retData = args;
if (complete >= waitTimes) {
clearTimeout(timeoutRef);
sendDataCallback();
}
},
};
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map());
}
if (!this.EventTask.get(ListenerMainName)?.get(ListenerSubName)) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
this.createListenerFunction(ListenerMainName);
});
}
const eventRetHandle = (eventData: any) => {
retEvent = eventData;
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
clearTimeout(timeoutRef);
reject(
new Error(
'EventChecker Failed: NTEvent serviceAndMethod:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n',
),
);
}
};
if (eventResult instanceof Promise) {
eventResult.then((eventResult: any) => {
eventRetHandle(eventResult);
})
.catch(reject);
} else {
eventRetHandle(eventResult);
}
},
async callNormalEventV2<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
> (
serviceAndMethod: `${Service}/${ServiceMethod}`,
listenerAndMethod: `${Listener}/${ListenerMethod}`,
args: Parameters<EventType>,
checkerEvent: (ret: Awaited<ReturnType<EventType>>) => boolean = () => true,
checkerListener: (...args: Parameters<ListenerType>) => boolean = () => true,
callbackTimesToWait = 1,
timeout = 5000
) {
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined;
let retEvent: any = {};
function sendDataCallback (resolve: any, reject: any) {
if (complete === 0) {
reject(
new Error(
'Timeout: NTEvent serviceAndMethod:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n'
)
);
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
}
}
const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0] ?? '';
const ListenerSubName = ListenerNameList[1] ?? '';
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
(resolve, reject) => {
const timeoutRef = setTimeout(() => sendDataCallback(resolve, reject), timeout);
const eventCallback = {
timeout,
createtime: Date.now(),
checker: checkerListener,
func: (...args: any[]) => {
complete++;
retData = args as Parameters<ListenerType>;
if (complete >= callbackTimesToWait) {
clearTimeout(timeoutRef);
sendDataCallback(resolve, reject);
}
},
};
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map());
}
if (!this.EventTask.get(ListenerMainName)?.get(ListenerSubName)) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
this.createListenerFunction(ListenerMainName);
const eventResult = this.createEventFunction(serviceAndMethod)!(...(args));
const eventRetHandle = (eventData: any) => {
retEvent = eventData;
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
clearTimeout(timeoutRef);
reject(
new Error(
'EventChecker Failed: NTEvent serviceAndMethod:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n'
)
);
}
};
if (eventResult instanceof Promise) {
eventResult.then((eventResult: any) => {
eventRetHandle(eventResult);
})
.catch(reject);
} else {
eventRetHandle(eventResult);
}
}
);
}
}

View File

@@ -2,41 +2,41 @@ type Handler<T> = () => T | Promise<T>;
type Checker<T> = (result: T) => T | Promise<T>;
export class Fallback<T> {
private handlers: Handler<T>[] = [];
private checker: Checker<T>;
private handlers: Handler<T>[] = [];
private checker: Checker<T>;
constructor(checker?: Checker<T>) {
this.checker = checker || (async (result: T) => result);
}
constructor (checker?: Checker<T>) {
this.checker = checker || (async (result: T) => result);
}
add(handler: Handler<T>): this {
this.handlers.push(handler);
return this;
}
add (handler: Handler<T>): this {
this.handlers.push(handler);
return this;
}
// 执行处理程序链
async run(): Promise<T> {
const errors: Error[] = [];
for (const handler of this.handlers) {
try {
const result = await handler();
const data = await this.checker(result);
if (data) {
return data;
}
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
// 执行处理程序链
async run (): Promise<T> {
const errors: Error[] = [];
for (const handler of this.handlers) {
try {
const result = await handler();
const data = await this.checker(result);
if (data) {
return data;
}
throw new AggregateError(errors, 'All handlers failed');
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
throw new AggregateError(errors, 'All handlers failed');
}
}
export class FallbackUtil {
static boolchecker<T>(value: T, condition: boolean): T {
if (condition) {
return value;
} else {
throw new Error('Condition is false, throwing error');
}
static boolchecker<T>(value: T, condition: boolean): T {
if (condition) {
return value;
} else {
throw new Error('Condition is false, throwing error');
}
}
}
}

View File

@@ -12,120 +12,119 @@ import type { IFFmpegAdapter } from './ffmpeg-adapter-interface';
* FFmpeg 适配器工厂
*/
export class FFmpegAdapterFactory {
private static instance: IFFmpegAdapter | null = null;
private static initPromise: Promise<IFFmpegAdapter> | null = null;
private static instance: IFFmpegAdapter | null = null;
private static initPromise: Promise<IFFmpegAdapter> | null = null;
/**
/**
* 初始化并获取最佳的 FFmpeg 适配器
* @param logger 日志记录器
* @param ffmpegPath FFmpeg 可执行文件路径(用于 Exec 适配器)
* @param ffprobePath FFprobe 可执行文件路径(用于 Exec 适配器)
* @param binaryPath 二进制文件路径(来自 pathWrapper.binaryPath,用于 Addon 适配器)
*/
static async getAdapter(
logger: LogWrapper,
ffmpegPath: string = 'ffmpeg',
ffprobePath: string = 'ffprobe',
binaryPath?: string
): Promise<IFFmpegAdapter> {
// 如果已经初始化,直接返回
if (this.instance) {
return this.instance;
}
// 如果正在初始化,等待初始化完成
if (this.initPromise) {
return this.initPromise;
}
// 开始初始化
this.initPromise = this.initialize(logger, ffmpegPath, ffprobePath, binaryPath);
try {
this.instance = await this.initPromise;
return this.instance;
} finally {
this.initPromise = null;
}
static async getAdapter (
logger: LogWrapper,
ffmpegPath: string = 'ffmpeg',
ffprobePath: string = 'ffprobe',
binaryPath?: string
): Promise<IFFmpegAdapter> {
// 如果已经初始化,直接返回
if (this.instance) {
return this.instance;
}
/**
// 如果正在初始化,等待初始化完成
if (this.initPromise) {
return this.initPromise;
}
// 开始初始化
this.initPromise = this.initialize(logger, ffmpegPath, ffprobePath, binaryPath);
try {
this.instance = await this.initPromise;
return this.instance;
} finally {
this.initPromise = null;
}
}
/**
* 初始化适配器
*/
private static async initialize(
logger: LogWrapper,
ffmpegPath: string,
ffprobePath: string,
binaryPath?: string
): Promise<IFFmpegAdapter> {
private static async initialize (
logger: LogWrapper,
ffmpegPath: string,
ffprobePath: string,
binaryPath?: string
): Promise<IFFmpegAdapter> {
// 1. 优先尝试使用 Native Addon
if (binaryPath) {
const addonAdapter = new FFmpegAddonAdapter(binaryPath);
// 1. 优先尝试使用 Native Addon
if (binaryPath) {
const addonAdapter = new FFmpegAddonAdapter(binaryPath);
logger.log('[FFmpeg] 检查 Native Addon 可用性...');
if (await addonAdapter.isAvailable()) {
logger.log('[FFmpeg] ✓ 使用 Native Addon 适配器');
return addonAdapter;
}
logger.log('[FFmpeg] 检查 Native Addon 可用性...');
if (await addonAdapter.isAvailable()) {
logger.log('[FFmpeg] ✓ 使用 Native Addon 适配器');
return addonAdapter;
}
logger.log('[FFmpeg] Native Addon 不可用,尝试使用命令行工具');
} else {
logger.log('[FFmpeg] 未提供 binaryPath跳过 Native Addon 检测');
}
// 2. 降级到 execFile 实现
const execAdapter = new FFmpegExecAdapter(ffmpegPath, ffprobePath, binaryPath, logger);
logger.log(`[FFmpeg] 检查命令行工具可用性: ${ffmpegPath}`);
if (await execAdapter.isAvailable()) {
logger.log('[FFmpeg] 使用命令行工具适配器 ✓');
return execAdapter;
}
// 3. 都不可用,返回 execAdapter 但会在使用时报错
logger.logError('[FFmpeg] 警告: FFmpeg 不可用,将使用命令行适配器但可能失败');
return execAdapter;
logger.log('[FFmpeg] Native Addon 可用,尝试使用命令行工具');
} else {
logger.log('[FFmpeg] 未提供 binaryPath跳过 Native Addon 检测');
}
/**
// 2. 降级到 execFile 实现
const execAdapter = new FFmpegExecAdapter(ffmpegPath, ffprobePath, binaryPath, logger);
logger.log(`[FFmpeg] 检查命令行工具可用性: ${ffmpegPath}`);
if (await execAdapter.isAvailable()) {
logger.log('[FFmpeg] 使用命令行工具适配器 ✓');
return execAdapter;
}
// 3. 都不可用,返回 execAdapter 但会在使用时报错
logger.logError('[FFmpeg] 警告: FFmpeg 不可用,将使用命令行适配器但可能失败');
return execAdapter;
}
/**
* 重置适配器(用于测试或重新初始化)
*/
static reset(): void {
this.instance = null;
this.initPromise = null;
}
static reset (): void {
this.instance = null;
this.initPromise = null;
}
/**
/**
* 更新 FFmpeg 路径并重新初始化
* @param logger 日志记录器
* @param ffmpegPath FFmpeg 可执行文件路径
* @param ffprobePath FFprobe 可执行文件路径
*/
static async updateFFmpegPath(
logger: LogWrapper,
ffmpegPath: string,
ffprobePath: string
): Promise<void> {
// 如果当前使用的是 Exec 适配器,更新路径
if (this.instance && this.instance instanceof FFmpegExecAdapter) {
logger.log(`[FFmpeg] 更新 FFmpeg 路径: ${ffmpegPath}`);
this.instance.setFFmpegPath(ffmpegPath);
this.instance.setFFprobePath(ffprobePath);
static async updateFFmpegPath (
logger: LogWrapper,
ffmpegPath: string,
ffprobePath: string
): Promise<void> {
// 如果当前使用的是 Exec 适配器,更新路径
if (this.instance && this.instance instanceof FFmpegExecAdapter) {
logger.log(`[FFmpeg] 更新 FFmpeg 路径: ${ffmpegPath}`);
this.instance.setFFmpegPath(ffmpegPath);
this.instance.setFFprobePath(ffprobePath);
// 验证新路径是否可用
if (await this.instance.isAvailable()) {
logger.log('[FFmpeg] 新路径验证成功 ✓');
} else {
logger.logError('[FFmpeg] 警告: 新 FFmpeg 路径不可用');
}
}
// 验证新路径是否可用
if (await this.instance.isAvailable()) {
logger.log('[FFmpeg] 新路径验证成功 ✓');
} else {
logger.logError('[FFmpeg] 警告: 新 FFmpeg 路径不可用');
}
}
}
/**
/**
* 获取当前适配器(不初始化)
*/
static getCurrentAdapter(): IFFmpegAdapter | null {
return this.instance;
}
static getCurrentAdapter (): IFFmpegAdapter | null {
return this.instance;
}
}

View File

@@ -7,62 +7,62 @@
* 视频信息结果
*/
export interface VideoInfoResult {
/** 视频宽度(像素) */
width: number;
/** 视频高度(像素) */
height: number;
/** 视频时长(秒) */
duration: number;
/** 容器格式 */
format: string;
/** 缩略图 Buffer */
thumbnail?: Buffer;
/** 视频宽度(像素) */
width: number;
/** 视频高度(像素) */
height: number;
/** 视频时长(秒) */
duration: number;
/** 容器格式 */
format: string;
/** 缩略图 Buffer */
thumbnail?: Buffer;
}
/**
* FFmpeg 适配器接口
*/
export interface IFFmpegAdapter {
/** 适配器名称 */
readonly name: string;
/** 适配器名称 */
readonly name: string;
/** 是否可用 */
isAvailable(): Promise<boolean>;
/** 是否可用 */
isAvailable(): Promise<boolean>;
/**
/**
* 获取视频信息(包含缩略图)
* @param videoPath 视频文件路径
* @returns 视频信息
*/
getVideoInfo(videoPath: string): Promise<VideoInfoResult>;
getVideoInfo(videoPath: string): Promise<VideoInfoResult>;
/**
/**
* 获取音视频文件时长
* @param filePath 文件路径
* @returns 时长(秒)
*/
getDuration(filePath: string): Promise<number>;
getDuration(filePath: string): Promise<number>;
/**
/**
* 转换音频为 PCM 格式
* @param filePath 输入文件路径
* @param pcmPath 输出 PCM 文件路径
* @returns PCM 数据 Buffer
*/
convertToPCM(filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }>;
convertToPCM(filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }>;
/**
/**
* 转换音频文件
* @param inputFile 输入文件路径
* @param outputFile 输出文件路径
* @param format 目标格式 ('amr' | 'silk' 等)
*/
convertFile(inputFile: string, outputFile: string, format: string): Promise<void>;
convertFile(inputFile: string, outputFile: string, format: string): Promise<void>;
/**
/**
* 提取视频缩略图
* @param videoPath 视频文件路径
* @param thumbnailPath 缩略图输出路径
*/
extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void>;
extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void>;
}

View File

@@ -15,108 +15,108 @@ import { dlopen } from 'node:process';
* 获取 Native Addon 路径
* @param binaryPath 二进制文件路径(来自 pathWrapper.binaryPath)
*/
function getAddonPath(binaryPath: string): string {
const platformName = platform();
const archName = arch();
function getAddonPath (binaryPath: string): string {
const platformName = platform();
const archName = arch();
let addonFileName: string = process.platform + '.' + process.arch;
let addonPath = path.join(binaryPath, "./native/ffmpeg/", `ffmpegAddon.${addonFileName}.node`);
if (!existsSync(addonPath)) {
throw new Error(`Unsupported platform: ${platformName} ${archName} - Addon not found at ${addonPath}`);
}
return addonPath;
const addonFileName: string = process.platform + '.' + process.arch;
const addonPath = path.join(binaryPath, './native/ffmpeg/', `ffmpegAddon.${addonFileName}.node`);
if (!existsSync(addonPath)) {
throw new Error(`Unsupported platform: ${platformName} ${archName} - Addon not found at ${addonPath}`);
}
return addonPath;
}
/**
* FFmpeg Native Addon 适配器实现
*/
export class FFmpegAddonAdapter implements IFFmpegAdapter {
public readonly name = 'FFmpegAddon';
private addon: FFmpeg | null = null;
private binaryPath: string;
public readonly name = 'FFmpegAddon';
private addon: FFmpeg | null = null;
private binaryPath: string;
constructor(binaryPath: string) {
this.binaryPath = binaryPath;
}
constructor (binaryPath: string) {
this.binaryPath = binaryPath;
}
/**
/**
* 检查 Addon 是否可用
*/
async isAvailable(): Promise<boolean> {
try {
let temp_addon = { exports: {} };
dlopen(temp_addon, getAddonPath(this.binaryPath));
this.addon = temp_addon.exports as FFmpeg;
return this.addon !== null;
} catch (error) {
console.log('[FFmpegAddonAdapter] Failed to load addon:', error);
return false;
}
async isAvailable (): Promise<boolean> {
try {
const temp_addon = { exports: {} };
dlopen(temp_addon, getAddonPath(this.binaryPath));
this.addon = temp_addon.exports as FFmpeg;
return this.addon !== null;
} catch (error) {
console.log('[FFmpegAddonAdapter] Failed to load addon:', error);
return false;
}
}
private ensureAddon(): FFmpeg {
if (!this.addon) {
throw new Error('FFmpeg Addon is not available');
}
return this.addon;
private ensureAddon (): FFmpeg {
if (!this.addon) {
throw new Error('FFmpeg Addon is not available');
}
return this.addon;
}
/**
/**
* 获取视频信息
*/
async getVideoInfo(videoPath: string): Promise<VideoInfoResult> {
const addon = this.ensureAddon();
const info = await addon.getVideoInfo(videoPath, 'bmp24');
async getVideoInfo (videoPath: string): Promise<VideoInfoResult> {
const addon = this.ensureAddon();
const info = await addon.getVideoInfo(videoPath, 'bmp24');
return {
width: info.width,
height: info.height,
duration: info.duration,
format: info.format,
thumbnail: info.image,
};
}
return {
width: info.width,
height: info.height,
duration: info.duration,
format: info.format,
thumbnail: info.image,
};
}
/**
/**
* 获取时长
*/
async getDuration(filePath: string): Promise<number> {
const addon = this.ensureAddon();
return addon.getDuration(filePath);
}
async getDuration (filePath: string): Promise<number> {
const addon = this.ensureAddon();
return addon.getDuration(filePath);
}
/**
/**
* 转换为 PCM
*/
async convertToPCM(filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }> {
const addon = this.ensureAddon();
const result = await addon.decodeAudioToPCM(filePath, pcmPath, 24000);
async convertToPCM (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }> {
const addon = this.ensureAddon();
const result = await addon.decodeAudioToPCM(filePath, pcmPath, 24000);
return result;
}
return result;
}
/**
/**
* 转换文件
*/
async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
const addon = this.ensureAddon();
async convertFile (inputFile: string, outputFile: string, format: string): Promise<void> {
const addon = this.ensureAddon();
if (format === 'silk' || format === 'ntsilk') {
// 使用 Addon 的 NTSILK 转换
await addon.convertToNTSilkTct(inputFile, outputFile);
} else {
throw new Error(`Format '${format}' is not supported by FFmpeg Addon`);
}
if (format === 'silk' || format === 'ntsilk') {
// 使用 Addon 的 NTSILK 转换
await addon.convertToNTSilkTct(inputFile, outputFile);
} else {
throw new Error(`Format '${format}' is not supported by FFmpeg Addon`);
}
}
/**
/**
* 提取缩略图
*/
async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const addon = this.ensureAddon();
const info = await addon.getVideoInfo(videoPath);
async extractThumbnail (videoPath: string, thumbnailPath: string): Promise<void> {
const addon = this.ensureAddon();
const info = await addon.getVideoInfo(videoPath);
// 将缩略图写入文件
await writeFile(thumbnailPath, info.image);
}
// 将缩略图写入文件
await writeFile(thumbnailPath, info.image);
}
}

View File

@@ -1,6 +1,6 @@
/**
* FFmpeg Node.js Native Addon Type Definitions
*
*
* This addon provides FFmpeg functionality for Node.js including:
* - Video information extraction with thumbnail generation
* - Audio/Video duration detection
@@ -12,60 +12,60 @@
* Video information result object
*/
export interface VideoInfo {
/** Video width in pixels */
width: number;
/** Video width in pixels */
width: number;
/** Video height in pixels */
height: number;
/** Video height in pixels */
height: number;
/** Video duration in seconds */
duration: number;
/** Video duration in seconds */
duration: number;
/** Container format name (e.g., "mp4", "mkv", "avi") */
format: string;
/** Container format name (e.g., "mp4", "mkv", "avi") */
format: string;
/** Video codec name (e.g., "h264", "hevc", "vp9") */
videoCodec: string;
/** Video codec name (e.g., "h264", "hevc", "vp9") */
videoCodec: string;
/** First frame thumbnail as BMP image buffer */
image: Buffer;
/** First frame thumbnail as BMP image buffer */
image: Buffer;
}
/**
* Audio PCM decoding result object
*/
export interface AudioPCMResult {
/** PCM audio data as 16-bit signed integer samples */
pcm: Buffer;
/** PCM audio data as 16-bit signed integer samples */
pcm: Buffer;
/** Sample rate in Hz (e.g., 44100, 48000, 24000) */
sampleRate: number;
/** Sample rate in Hz (e.g., 44100, 48000, 24000) */
sampleRate: number;
/** Number of audio channels (1 for mono, 2 for stereo) */
channels: number;
/** Number of audio channels (1 for mono, 2 for stereo) */
channels: number;
}
/**
* FFmpeg interface providing all audio/video processing methods
*/
export interface FFmpeg {
/**
/**
* Get video information including resolution, duration, format, codec and first frame thumbnail
*/
getVideoInfo(filePath: string, format?: 'bmp' | 'bmp24'): Promise<VideoInfo>;
getVideoInfo(filePath: string, format?: 'bmp' | 'bmp24'): Promise<VideoInfo>;
/**
/**
* Get duration of audio or video file in seconds
*/
getDuration(filePath: string): Promise<number>;
getDuration(filePath: string): Promise<number>;
/**
/**
* Convert audio file to NTSILK format (WeChat voice message format)
*/
convertToNTSilkTct(inputPath: string, outputPath: string): Promise<void>;
convertToNTSilkTct(inputPath: string, outputPath: string): Promise<void>;
/**
/**
* Decode audio file to raw PCM data
*/
decodeAudioToPCM(filePath: string, pcmPath: string, sampleRate?: number): Promise<{ result: boolean, sampleRate: number }>;
}
decodeAudioToPCM(filePath: string, pcmPath: string, sampleRate?: number): Promise<{ result: boolean, sampleRate: number }>;
}

View File

@@ -18,227 +18,227 @@ const execFileAsync = promisify(execFile);
/**
* 确保目录存在
*/
function ensureDirExists(filePath: string): void {
const dir = dirname(filePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
function ensureDirExists (filePath: string): void {
const dir = dirname(filePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
/**
* FFmpeg 命令行适配器实现
*/
export class FFmpegExecAdapter implements IFFmpegAdapter {
public readonly name = 'FFmpegExec';
private downloadAttempted = false;
constructor(
private ffmpegPath: string = 'ffmpeg',
private ffprobePath: string = 'ffprobe',
private binaryPath?: string,
private logger?: LogWrapper
) {}
public readonly name = 'FFmpegExec';
private downloadAttempted = false;
/**
constructor (
private ffmpegPath: string = 'ffmpeg',
private ffprobePath: string = 'ffprobe',
private binaryPath?: string,
private logger?: LogWrapper
) { }
/**
* 检查 FFmpeg 是否可用,如果不可用则尝试下载
*/
async isAvailable(): Promise<boolean> {
// 首先检查当前路径
try {
await execFileAsync(this.ffmpegPath, ['-version']);
return true;
} catch {
// 如果失败且未尝试下载,尝试下载
if (!this.downloadAttempted && this.binaryPath && this.logger) {
this.downloadAttempted = true;
if (process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
return false;
}
this.logger.log('[FFmpeg] 未找到可用的 FFmpeg尝试自动下载...');
const result = await downloadFFmpegIfNotExists(this.logger);
if (result.path && result.reset) {
// 更新路径
if (process.platform === 'win32') {
this.ffmpegPath = join(result.path, 'ffmpeg.exe');
this.ffprobePath = join(result.path, 'ffprobe.exe');
this.logger.log('[FFmpeg] 已更新路径:', this.ffmpegPath);
// 再次检查
try {
await execFileAsync(this.ffmpegPath, ['-version']);
return true;
} catch {
return false;
}
}
}
}
return false;
}
}
async isAvailable (): Promise<boolean> {
// 首先检查当前路径
try {
await execFileAsync(this.ffmpegPath, ['-version']);
return true;
} catch {
// 如果失败且未尝试下载,尝试下载
if (!this.downloadAttempted && this.binaryPath && this.logger) {
this.downloadAttempted = true;
/**
if (process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
return false;
}
this.logger.log('[FFmpeg] 未找到可用的 FFmpeg尝试自动下载...');
const result = await downloadFFmpegIfNotExists(this.logger);
if (result.path && result.reset) {
// 更新路径
if (process.platform === 'win32') {
this.ffmpegPath = join(result.path, 'ffmpeg.exe');
this.ffprobePath = join(result.path, 'ffprobe.exe');
this.logger.log('[FFmpeg] 已更新路径:', this.ffmpegPath);
// 再次检查
try {
await execFileAsync(this.ffmpegPath, ['-version']);
return true;
} catch {
return false;
}
}
}
}
return false;
}
}
/**
* 设置 FFmpeg 路径
*/
setFFmpegPath(ffmpegPath: string): void {
this.ffmpegPath = ffmpegPath;
}
setFFmpegPath (ffmpegPath: string): void {
this.ffmpegPath = ffmpegPath;
}
/**
/**
* 设置 FFprobe 路径
*/
setFFprobePath(ffprobePath: string): void {
this.ffprobePath = ffprobePath;
}
setFFprobePath (ffprobePath: string): void {
this.ffprobePath = ffprobePath;
}
/**
/**
* 获取视频信息
*/
async getVideoInfo(videoPath: string): Promise<VideoInfoResult> {
// 获取文件大小和类型
const [fileType, duration] = await Promise.all([
fileTypeFromFile(videoPath).catch(() => null),
this.getDuration(videoPath)
]);
async getVideoInfo (videoPath: string): Promise<VideoInfoResult> {
// 获取文件大小和类型
const [fileType, duration] = await Promise.all([
fileTypeFromFile(videoPath).catch(() => null),
this.getDuration(videoPath),
]);
// 创建临时缩略图路径
const thumbnailPath = `${videoPath}.thumbnail.bmp`;
let width = 100;
let height = 100;
let thumbnail: Buffer | undefined;
// 创建临时缩略图路径
const thumbnailPath = `${videoPath}.thumbnail.bmp`;
let width = 100;
let height = 100;
let thumbnail: Buffer | undefined;
try {
await this.extractThumbnail(videoPath, thumbnailPath);
// 获取图片尺寸
const dimensions = await imageSizeFallBack(thumbnailPath);
width = dimensions.width ?? 100;
height = dimensions.height ?? 100;
// 读取缩略图
if (existsSync(thumbnailPath)) {
thumbnail = readFileSync(thumbnailPath);
}
} catch (error) {
// 使用默认值
}
try {
await this.extractThumbnail(videoPath, thumbnailPath);
return {
width,
height,
duration,
format: fileType?.ext ?? 'mp4',
thumbnail,
};
// 获取图片尺寸
const dimensions = await imageSizeFallBack(thumbnailPath);
width = dimensions.width ?? 100;
height = dimensions.height ?? 100;
// 读取缩略图
if (existsSync(thumbnailPath)) {
thumbnail = readFileSync(thumbnailPath);
}
} catch (_error) {
// 使用默认值
}
/**
return {
width,
height,
duration,
format: fileType?.ext ?? 'mp4',
thumbnail,
};
}
/**
* 获取时长
*/
async getDuration(filePath: string): Promise<number> {
try {
const { stdout } = await execFileAsync(this.ffprobePath, [
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
filePath
]);
async getDuration (filePath: string): Promise<number> {
try {
const { stdout } = await execFileAsync(this.ffprobePath, [
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
filePath,
]);
const duration = parseFloat(stdout.trim());
return isNaN(duration) ? 60 : duration;
} catch {
return 60; // 默认时长
}
const duration = parseFloat(stdout.trim());
return isNaN(duration) ? 60 : duration;
} catch {
return 60; // 默认时长
}
}
/**
/**
* 转换为 PCM
*/
async convertToPCM(filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }> {
try {
ensureDirExists(pcmPath);
async convertToPCM (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number; }> {
try {
ensureDirExists(pcmPath);
await execFileAsync(this.ffmpegPath, [
'-y',
'-i', filePath,
'-ar', '24000',
'-ac', '1',
'-f', 's16le',
pcmPath
]);
await execFileAsync(this.ffmpegPath, [
'-y',
'-i', filePath,
'-ar', '24000',
'-ac', '1',
'-f', 's16le',
pcmPath,
]);
if (!existsSync(pcmPath)) {
throw new Error('转换PCM失败输出文件不存在');
}
if (!existsSync(pcmPath)) {
throw new Error('转换PCM失败输出文件不存在');
}
return { result: true, sampleRate: 24000 };
} catch (error: any) {
throw new Error(`FFmpeg处理转换出错: ${error.message}`);
}
return { result: true, sampleRate: 24000 };
} catch (error: any) {
throw new Error(`FFmpeg处理转换出错: ${error.message}`);
}
}
/**
/**
* 转换文件
*/
async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
try {
ensureDirExists(outputFile);
async convertFile (inputFile: string, outputFile: string, format: string): Promise<void> {
try {
ensureDirExists(outputFile);
const params = format === 'amr'
? [
'-f', 's16le',
'-ar', '24000',
'-ac', '1',
'-i', inputFile,
'-ar', '8000',
'-b:a', '12.2k',
'-y',
outputFile
]
: [
'-f', 's16le',
'-ar', '24000',
'-ac', '1',
'-i', inputFile,
'-y',
outputFile
];
const params = format === 'amr'
? [
'-f', 's16le',
'-ar', '24000',
'-ac', '1',
'-i', inputFile,
'-ar', '8000',
'-b:a', '12.2k',
'-y',
outputFile,
]
: [
'-f', 's16le',
'-ar', '24000',
'-ac', '1',
'-i', inputFile,
'-y',
outputFile,
];
await execFileAsync(this.ffmpegPath, params);
await execFileAsync(this.ffmpegPath, params);
if (!existsSync(outputFile)) {
throw new Error('转换失败,输出文件不存在');
}
} catch (error) {
console.error('Error converting file:', error);
throw new Error(`文件转换失败: ${(error as Error).message}`);
}
if (!existsSync(outputFile)) {
throw new Error('转换失败,输出文件不存在');
}
} catch (error) {
console.error('Error converting file:', error);
throw new Error(`文件转换失败: ${(error as Error).message}`);
}
}
/**
/**
* 提取缩略图
*/
async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
try {
ensureDirExists(thumbnailPath);
async extractThumbnail (videoPath: string, thumbnailPath: string): Promise<void> {
try {
ensureDirExists(thumbnailPath);
const { stderr } = await execFileAsync(this.ffmpegPath, [
'-i', videoPath,
'-ss', '00:00:01.000',
'-vframes', '1',
'-y', // 覆盖输出文件
thumbnailPath
]);
const { stderr } = await execFileAsync(this.ffmpegPath, [
'-i', videoPath,
'-ss', '00:00:01.000',
'-vframes', '1',
'-y', // 覆盖输出文件
thumbnailPath,
]);
if (!existsSync(thumbnailPath)) {
throw new Error(`提取缩略图失败,输出文件不存在: ${stderr}`);
}
} catch (error) {
console.error('Error extracting thumbnail:', error);
throw new Error(`提取缩略图失败: ${(error as Error).message}`);
}
if (!existsSync(thumbnailPath)) {
throw new Error(`提取缩略图失败,输出文件不存在: ${stderr}`);
}
} catch (error) {
console.error('Error extracting thumbnail:', error);
throw new Error(`提取缩略图失败: ${(error as Error).message}`);
}
}
}

View File

@@ -8,137 +8,137 @@ import { FFmpegAdapterFactory } from './ffmpeg-adapter-factory';
import type { IFFmpegAdapter } from './ffmpeg-adapter-interface';
const getFFmpegPath = (tool: string, binaryPath?: string): string => {
if (process.platform === 'win32' && binaryPath) {
const exeName = `${tool}.exe`;
const localPath = path.join(binaryPath, 'ffmpeg', exeName);
const isLocalExeExists = existsSync(localPath);
return isLocalExeExists ? localPath : exeName;
}
return tool;
if (process.platform === 'win32' && binaryPath) {
const exeName = `${tool}.exe`;
const localPath = path.join(binaryPath, 'ffmpeg', exeName);
const isLocalExeExists = existsSync(localPath);
return isLocalExeExists ? localPath : exeName;
}
return tool;
};
export let FFMPEG_CMD = 'ffmpeg';
export let FFPROBE_CMD = 'ffprobe';
export class FFmpegService {
private static adapter: IFFmpegAdapter | null = null;
private static initialized = false;
private static adapter: IFFmpegAdapter | null = null;
private static initialized = false;
/**
/**
* 初始化 FFmpeg 服务
* @param binaryPath 二进制文件路径(来自 pathWrapper.binaryPath)
* @param logger 日志记录器
*/
public static async init(binaryPath: string, logger: LogWrapper): Promise<void> {
if (this.initialized) {
return;
}
// 检查本地 ffmpeg 路径
FFMPEG_CMD = getFFmpegPath('ffmpeg', binaryPath);
FFPROBE_CMD = getFFmpegPath('ffprobe', binaryPath);
// 立即初始化适配器(会触发自动下载等逻辑)
this.adapter = await FFmpegAdapterFactory.getAdapter(
logger,
FFMPEG_CMD,
FFPROBE_CMD,
binaryPath
);
this.initialized = true;
public static async init (binaryPath: string, logger: LogWrapper): Promise<void> {
if (this.initialized) {
return;
}
/**
// 检查本地 ffmpeg 路径
FFMPEG_CMD = getFFmpegPath('ffmpeg', binaryPath);
FFPROBE_CMD = getFFmpegPath('ffprobe', binaryPath);
// 立即初始化适配器(会触发自动下载等逻辑)
this.adapter = await FFmpegAdapterFactory.getAdapter(
logger,
FFMPEG_CMD,
FFPROBE_CMD,
binaryPath
);
this.initialized = true;
}
/**
* 获取 FFmpeg 适配器
*/
private static async getAdapter(): Promise<IFFmpegAdapter> {
if (!this.adapter) {
throw new Error('FFmpeg service not initialized. Please call FFmpegService.init() first.');
}
return this.adapter;
private static async getAdapter (): Promise<IFFmpegAdapter> {
if (!this.adapter) {
throw new Error('FFmpeg service not initialized. Please call FFmpegService.init() first.');
}
return this.adapter;
}
/**
/**
* 设置 FFmpeg 路径并更新适配器
* @deprecated 建议使用 init() 方法初始化
*/
public static async setFfmpegPath(ffmpegPath: string, logger: LogWrapper): Promise<void> {
if (platform() === 'win32') {
FFMPEG_CMD = path.join(ffmpegPath, 'ffmpeg.exe');
FFPROBE_CMD = path.join(ffmpegPath, 'ffprobe.exe');
logger.log('[Check] ffmpeg:', FFMPEG_CMD);
logger.log('[Check] ffprobe:', FFPROBE_CMD);
public static async setFfmpegPath (ffmpegPath: string, logger: LogWrapper): Promise<void> {
if (platform() === 'win32') {
FFMPEG_CMD = path.join(ffmpegPath, 'ffmpeg.exe');
FFPROBE_CMD = path.join(ffmpegPath, 'ffprobe.exe');
logger.log('[Check] ffmpeg:', FFMPEG_CMD);
logger.log('[Check] ffprobe:', FFPROBE_CMD);
// 更新适配器路径
await FFmpegAdapterFactory.updateFFmpegPath(logger, FFMPEG_CMD, FFPROBE_CMD);
}
// 更新适配器路径
await FFmpegAdapterFactory.updateFFmpegPath(logger, FFMPEG_CMD, FFPROBE_CMD);
}
}
/**
/**
* 提取视频缩略图
*/
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const adapter = await this.getAdapter();
await adapter.extractThumbnail(videoPath, thumbnailPath);
}
public static async extractThumbnail (videoPath: string, thumbnailPath: string): Promise<void> {
const adapter = await this.getAdapter();
await adapter.extractThumbnail(videoPath, thumbnailPath);
}
/**
/**
* 转换音频文件
*/
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
const adapter = await this.getAdapter();
await adapter.convertFile(inputFile, outputFile, format);
}
public static async convertFile (inputFile: string, outputFile: string, format: string): Promise<void> {
const adapter = await this.getAdapter();
await adapter.convertFile(inputFile, outputFile, format);
}
/**
/**
* 转换为 PCM 格式
*/
public static async convert(filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }> {
const adapter = await this.getAdapter();
return adapter.convertToPCM(filePath, pcmPath);
}
public static async convert (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number; }> {
const adapter = await this.getAdapter();
return adapter.convertToPCM(filePath, pcmPath);
}
/**
/**
* 获取视频信息
*/
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
const adapter = await this.getAdapter();
public static async getVideoInfo (videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
const adapter = await this.getAdapter();
try {
// 获取文件大小
const fileSize = statSync(videoPath).size;
try {
// 获取文件大小
const fileSize = statSync(videoPath).size;
// 使用适配器获取视频信息
const videoInfo = await adapter.getVideoInfo(videoPath);
// 使用适配器获取视频信息
const videoInfo = await adapter.getVideoInfo(videoPath);
// 如果提供了缩略图路径且适配器返回了缩略图,保存到指定路径
if (thumbnailPath && videoInfo.thumbnail) {
writeFileSync(thumbnailPath, videoInfo.thumbnail);
}
// 如果提供了缩略图路径且适配器返回了缩略图,保存到指定路径
if (thumbnailPath && videoInfo.thumbnail) {
writeFileSync(thumbnailPath, videoInfo.thumbnail);
}
const result: VideoInfo = {
width: videoInfo.width,
height: videoInfo.height,
time: videoInfo.duration,
format: videoInfo.format,
size: fileSize,
filePath: videoPath
};
const result: VideoInfo = {
width: videoInfo.width,
height: videoInfo.height,
time: videoInfo.duration,
format: videoInfo.format,
size: fileSize,
filePath: videoPath,
};
return result;
} catch (error) {
// 降级处理:返回默认值
const fileType = await fileTypeFromFile(videoPath).catch(() => null);
const fileSize = statSync(videoPath).size;
return result;
} catch (_error) {
// 降级处理:返回默认值
const fileType = await fileTypeFromFile(videoPath).catch(() => null);
const fileSize = statSync(videoPath).size;
return {
width: 100,
height: 100,
time: 60,
format: fileType?.ext ?? 'mp4',
size: fileSize,
filePath: videoPath
};
}
return {
width: 100,
height: 100,
time: 60,
format: fileType?.ext ?? 'mp4',
size: fileSize,
filePath: videoPath,
};
}
}
}
}

View File

@@ -2,120 +2,120 @@ import { Peer } from '@/core';
import { randomUUID } from 'crypto';
class TimeBasedCache<K, V> {
private cache = new Map<K, { value: V, timestamp: number, frequency: number }>();
private keyList = new Set<K>();
private operationCount = 0;
private cache = new Map<K, { value: V, timestamp: number, frequency: number }>();
private keyList = new Set<K>();
private operationCount = 0;
constructor(private maxCapacity: number, private ttl: number = 30 * 1000 * 60, private cleanupCount: number = 10) {}
constructor (private maxCapacity: number, private ttl: number = 30 * 1000 * 60, private cleanupCount: number = 10) {}
public put(key: K, value: V): void {
const timestamp = Date.now();
const cacheEntry = { value, timestamp, frequency: 1 };
this.cache.set(key, cacheEntry);
this.keyList.add(key);
this.operationCount++;
if (this.keyList.size > this.maxCapacity) this.evict();
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
public put (key: K, value: V): void {
const timestamp = Date.now();
const cacheEntry = { value, timestamp, frequency: 1 };
this.cache.set(key, cacheEntry);
this.keyList.add(key);
this.operationCount++;
if (this.keyList.size > this.maxCapacity) this.evict();
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
}
public get (key: K): V | undefined {
const entry = this.cache.get(key);
if (entry && Date.now() - entry.timestamp < this.ttl) {
entry.timestamp = Date.now();
entry.frequency++;
this.operationCount++;
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
return entry.value;
} else {
this.deleteKey(key);
}
return undefined;
}
public get(key: K): V | undefined {
private cleanup (count: number): void {
const currentTime = Date.now();
let cleaned = 0;
for (const key of this.keyList) {
if (cleaned >= count) break;
const entry = this.cache.get(key);
if (entry && currentTime - entry.timestamp >= this.ttl) {
this.deleteKey(key);
cleaned++;
}
}
this.operationCount = 0; // 重置操作计数器
}
private deleteKey (key: K): void {
this.cache.delete(key);
this.keyList.delete(key);
}
private evict (): void {
while (this.keyList.size > this.maxCapacity) {
let oldestKey: K | undefined;
let minFrequency = Infinity;
for (const key of this.keyList) {
const entry = this.cache.get(key);
if (entry && Date.now() - entry.timestamp < this.ttl) {
entry.timestamp = Date.now();
entry.frequency++;
this.operationCount++;
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
return entry.value;
} else {
this.deleteKey(key);
}
return undefined;
}
private cleanup(count: number): void {
const currentTime = Date.now();
let cleaned = 0;
for (const key of this.keyList) {
if (cleaned >= count) break;
const entry = this.cache.get(key);
if (entry && currentTime - entry.timestamp >= this.ttl) {
this.deleteKey(key);
cleaned++;
}
}
this.operationCount = 0; // 重置操作计数器
}
private deleteKey(key: K): void {
this.cache.delete(key);
this.keyList.delete(key);
}
private evict(): void {
while (this.keyList.size > this.maxCapacity) {
let oldestKey: K | undefined;
let minFrequency = Infinity;
for (const key of this.keyList) {
const entry = this.cache.get(key);
if (entry && entry.frequency < minFrequency) {
minFrequency = entry.frequency;
oldestKey = key;
}
}
if (oldestKey !== undefined) this.deleteKey(oldestKey);
if (entry && entry.frequency < minFrequency) {
minFrequency = entry.frequency;
oldestKey = key;
}
}
if (oldestKey !== undefined) this.deleteKey(oldestKey);
}
}
}
interface FileUUIDData {
peer: Peer;
modelId?: string;
fileId?: string;
msgId?: string;
elementId?: string;
fileUUID?: string;
peer: Peer;
modelId?: string;
fileId?: string;
msgId?: string;
elementId?: string;
fileUUID?: string;
}
class FileUUIDManager {
private cache: TimeBasedCache<string, FileUUIDData>;
private cache: TimeBasedCache<string, FileUUIDData>;
constructor(ttl: number) {
this.cache = new TimeBasedCache<string, FileUUIDData>(5000, ttl);
}
constructor (ttl: number) {
this.cache = new TimeBasedCache<string, FileUUIDData>(5000, ttl);
}
public encode(data: FileUUIDData, endString: string = '', customUUID?: string): string {
const uuid = customUUID ? customUUID : randomUUID().replace(/-/g, '') + endString;
this.cache.put(uuid, data);
return uuid;
}
public encode (data: FileUUIDData, endString: string = '', customUUID?: string): string {
const uuid = customUUID || randomUUID().replace(/-/g, '') + endString;
this.cache.put(uuid, data);
return uuid;
}
public decode(uuid: string): FileUUIDData | undefined {
return this.cache.get(uuid);
}
public decode (uuid: string): FileUUIDData | undefined {
return this.cache.get(uuid);
}
}
export class FileNapCatOneBotUUIDWrap {
private manager: FileUUIDManager;
private manager: FileUUIDManager;
constructor(ttl: number = 86400000) {
this.manager = new FileUUIDManager(ttl);
}
constructor (ttl: number = 86400000) {
this.manager = new FileUUIDManager(ttl);
}
public encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = '', endString: string = '', customUUID?: string): string {
return this.manager.encode({ peer, modelId, fileId, fileUUID }, endString, customUUID);
}
public encodeModelId (peer: Peer, modelId: string, fileId: string, fileUUID: string = '', endString: string = '', customUUID?: string): string {
return this.manager.encode({ peer, modelId, fileId, fileUUID }, endString, customUUID);
}
public decodeModelId(uuid: string): FileUUIDData | undefined {
return this.manager.decode(uuid);
}
public decodeModelId (uuid: string): FileUUIDData | undefined {
return this.manager.decode(uuid);
}
public encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = '', customUUID?: string): string {
return this.manager.encode({ peer, msgId, elementId, fileUUID }, '', customUUID);
}
public encode (peer: Peer, msgId: string, elementId: string, fileUUID: string = '', customUUID?: string): string {
return this.manager.encode({ peer, msgId, elementId, fileUUID }, '', customUUID);
}
public decode(uuid: string): FileUUIDData | undefined {
return this.manager.decode(uuid);
}
public decode (uuid: string): FileUUIDData | undefined {
return this.manager.decode(uuid);
}
}
export const FileNapCatOneBotUUID = new FileNapCatOneBotUUIDWrap();
export const FileNapCatOneBotUUID = new FileNapCatOneBotUUIDWrap();

View File

@@ -5,205 +5,205 @@ import path from 'node:path';
import { solveProblem } from '@/common/helper';
export interface HttpDownloadOptions {
url: string;
headers?: Record<string, string> | string;
url: string;
headers?: Record<string, string> | string;
}
type Uri2LocalRes = {
success: boolean,
errMsg: string,
fileName: string,
path: string
success: boolean,
errMsg: string,
fileName: string,
path: string;
};
// 定义一个异步函数来检查文件是否存在
export function checkFileExist (path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check () {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
// 定义一个异步函数来检查文件是否存在
export function checkFileExist(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
// 定义一个异步函数来检查文件是否存在
export async function checkFileExistV2(path: string, timeout: number = 3000): Promise<void> {
// 使用 Promise.race 来同时进行文件状态检查和超时计时
// Promise.race 会返回第一个解决resolve或拒绝reject的 Promise
await Promise.race([
checkFile(path),
timeoutPromise(timeout, `文件不存在: ${path}`),
]);
export async function checkFileExistV2 (path: string, timeout: number = 3000): Promise<void> {
// 使用 Promise.race 来同时进行文件状态检查和超时计时
// Promise.race 会返回第一个解决resolve或拒绝reject的 Promise
await Promise.race([
checkFile(path),
timeoutPromise(timeout, `文件不存在: ${path}`),
]);
}
// 转换超时时间至 Promise
function timeoutPromise(timeout: number, errorMsg: string): Promise<void> {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(errorMsg));
}, timeout);
});
function timeoutPromise (timeout: number, errorMsg: string): Promise<void> {
return new Promise((_resolve, reject) => {
setTimeout(() => {
reject(new Error(errorMsg));
}, timeout);
});
}
// 异步检查文件是否存在
async function checkFile(path: string): Promise<void> {
try {
await stat(path);
} catch (error: unknown) {
if ((error as Error & { code: string }).code === 'ENOENT') {
// 如果文件不存在,则抛出一个错误
throw new Error(`文件不存在: ${path}`);
} else {
// 对于 stat 调用的其他错误,重新抛出
throw error;
}
}
// 如果文件存在则无需做任何事情Promise 解决resolve自身
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
}
async function tryDownload(options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
let url: string;
let headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
};
if (typeof options === 'string') {
url = options;
headers['Host'] = new URL(url).hostname;
async function checkFile (path: string): Promise<void> {
try {
await stat(path);
} catch (error: unknown) {
if ((error as Error & { code: string; }).code === 'ENOENT') {
// 如果文件不存在,则抛出一个错误
throw new Error(`文件不存在: ${path}`);
} else {
url = options.url;
if (options.headers) {
if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers);
} else {
headers = options.headers;
}
}
// 对于 stat 调用的其他错误,重新抛出
throw error;
}
if (useReferer && !headers['Referer']) {
headers['Referer'] = url;
}
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
if (err.cause) {
throw err.cause;
}
throw err;
});
return fetchRes;
}
// 如果文件存在则无需做任何事情Promise 解决resolve自身
}
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
const useReferer = typeof options === 'string';
let resp = await tryDownload(options);
if (resp.status === 403 && useReferer) {
resp = await tryDownload(options, true);
export function calculateFileMD5 (filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
}
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
let url: string;
let headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
};
if (typeof options === 'string') {
url = options;
headers['Host'] = new URL(url).hostname;
} else {
url = options.url;
if (options.headers) {
if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers);
} else {
headers = options.headers;
}
}
if (!resp.ok) throw new Error(`下载文件失败: ${resp.statusText}`);
const blob = await resp.blob();
const buffer = await blob.arrayBuffer();
return Buffer.from(buffer);
}
if (useReferer && !headers['Referer']) {
headers['Referer'] = url;
}
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
if (err.cause) {
throw err.cause;
}
throw err;
});
return fetchRes;
}
export async function httpDownload (options: string | HttpDownloadOptions): Promise<Buffer> {
const useReferer = typeof options === 'string';
let resp = await tryDownload(options);
if (resp.status === 403 && useReferer) {
resp = await tryDownload(options, true);
}
if (!resp.ok) throw new Error(`下载文件失败: ${resp.statusText}`);
const blob = await resp.blob();
const buffer = await blob.arrayBuffer();
return Buffer.from(buffer);
}
export enum FileUriType {
Unknown = 0,
Local = 1,
Remote = 2,
Base64 = 3
Unknown = 0,
Local = 1,
Remote = 2,
Base64 = 3,
}
export async function checkUriType(Uri: string) {
const LocalFileRet = await solveProblem((uri: string) => {
if (fs.existsSync(path.normalize(uri))) {
return { Uri: path.normalize(uri), Type: FileUriType.Local };
}
return undefined;
}, Uri);
if (LocalFileRet) return LocalFileRet;
const OtherFileRet = await solveProblem((uri: string) => {
// 再判断是否是Http
if (uri.startsWith('http:') || uri.startsWith('https:')) {
return { Uri: uri, Type: FileUriType.Remote };
}
// 再判断是否是Base64
if (uri.startsWith('base64:')) {
return { Uri: uri, Type: FileUriType.Base64 };
}
// 默认file://
if (uri.startsWith('file:')) {
const filePath: string = decodeURIComponent(uri.startsWith('file:///') && process.platform === 'win32' ? uri.slice(8) : uri.slice(7));
return { Uri: filePath, Type: FileUriType.Local };
}
if (uri.startsWith('data:')) {
const data = uri.split(',')[1];
if (data) return { Uri: data, Type: FileUriType.Base64 };
}
return;
}, Uri);
if (OtherFileRet) return OtherFileRet;
return { Uri: Uri, Type: FileUriType.Unknown };
}
export async function uriToLocalFile(dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
const filePath = path.join(dir, filename);
switch (UriType) {
case FileUriType.Local: {
const fileExt = path.extname(HandledUri);
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
const tempFilePath = path.join(dir, filename + fileExt);
fs.copyFileSync(HandledUri, tempFilePath);
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
}
case FileUriType.Remote: {
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
fs.writeFileSync(filePath, buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
case FileUriType.Base64: {
const base64 = HandledUri.replace(/^base64:\/\//, '');
const base64Buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(filePath, base64Buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
export async function checkUriType (Uri: string) {
const LocalFileRet = await solveProblem((uri: string) => {
if (fs.existsSync(path.normalize(uri))) {
return { Uri: path.normalize(uri), Type: FileUriType.Local };
}
return undefined;
}, Uri);
if (LocalFileRet) return LocalFileRet;
const OtherFileRet = await solveProblem((uri: string) => {
// 再判断是否是Http
if (uri.startsWith('http:') || uri.startsWith('https:')) {
return { Uri: uri, Type: FileUriType.Remote };
}
// 再判断是否是Base64
if (uri.startsWith('base64:')) {
return { Uri: uri, Type: FileUriType.Base64 };
}
// 默认file://
if (uri.startsWith('file:')) {
const filePath: string = decodeURIComponent(uri.startsWith('file:///') && process.platform === 'win32' ? uri.slice(8) : uri.slice(7));
return { Uri: filePath, Type: FileUriType.Local };
}
if (uri.startsWith('data:')) {
const data = uri.split(',')[1];
if (data) return { Uri: data, Type: FileUriType.Base64 };
}
return undefined;
}, Uri);
if (OtherFileRet) return OtherFileRet;
return { Uri, Type: FileUriType.Unknown };
}
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
const filePath = path.join(dir, filename);
switch (UriType) {
case FileUriType.Local: {
const fileExt = path.extname(HandledUri);
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
const tempFilePath = path.join(dir, filename + fileExt);
fs.copyFileSync(HandledUri, tempFilePath);
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
}
case FileUriType.Remote: {
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
fs.writeFileSync(filePath, buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
case FileUriType.Base64: {
const base64 = HandledUri.replace(/^base64:\/\//, '');
const base64Buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(filePath, base64Buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
}
}

View File

@@ -2,113 +2,115 @@ import * as crypto from 'node:crypto';
import { PacketMsg } from '@/core/packet/message/message';
interface ForwardMsgJson {
app: string
config: ForwardMsgJsonConfig,
desc: string,
extra: ForwardMsgJsonExtra,
meta: ForwardMsgJsonMeta,
prompt: string,
ver: string,
view: string
app: string
config: ForwardMsgJsonConfig,
desc: string,
extra: ForwardMsgJsonExtra,
meta: ForwardMsgJsonMeta,
prompt: string,
ver: string,
view: string
}
interface ForwardMsgJsonConfig {
autosize: number,
forward: number,
round: number,
type: string,
width: number
autosize: number,
forward: number,
round: number,
type: string,
width: number
}
interface ForwardMsgJsonExtra {
filename: string,
tsum: number,
filename: string,
tsum: number,
}
interface ForwardMsgJsonMeta {
detail: ForwardMsgJsonMetaDetail
detail: ForwardMsgJsonMetaDetail
}
interface ForwardMsgJsonMetaDetail {
news: {
text: string
}[],
resid: string,
source: string,
summary: string,
uniseq: string
news: {
text: string
}[],
resid: string,
source: string,
summary: string,
uniseq: string
}
interface ForwardAdaptMsg {
senderName?: string;
isGroupMsg?: boolean;
msg?: ForwardAdaptMsgElement[];
senderName?: string;
isGroupMsg?: boolean;
msg?: ForwardAdaptMsgElement[];
}
interface ForwardAdaptMsgElement {
preview?: string;
preview?: string;
}
export class ForwardMsgBuilder {
private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
const id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) {
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
}
if (!news) {
news = msg.length === 0 ? [{
text: 'Nya~ This message is send from NapCat.Packet!',
}] : msg.map(m => ({
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
}));
}
if (!summary) {
summary = `查看${msg.length}条转发消息`;
}
if (!prompt) {
prompt = '[聊天记录]';
}
return {
app: 'com.tencent.multimsg',
config: {
autosize: 1,
forward: 1,
round: 1,
type: 'normal',
width: 300
},
desc: prompt,
extra: {
filename: id,
tsum: msg.length,
},
meta: {
detail: {
news,
resid: resId,
source,
summary,
uniseq: id,
}
},
prompt,
ver: '0.0.0.5',
view: 'contact',
};
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
const id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) {
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
}
if (!news) {
news = msg.length === 0
? [{
text: 'Nya~ This message is send from NapCat.Packet!',
}]
: msg.map(m => ({
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
}));
}
if (!summary) {
summary = `查看${msg.length}条转发消息`;
}
if (!prompt) {
prompt = '[聊天记录]';
}
return {
app: 'com.tencent.multimsg',
config: {
autosize: 1,
forward: 1,
round: 1,
type: 'normal',
width: 300,
},
desc: prompt,
extra: {
filename: id,
tsum: msg.length,
},
meta: {
detail: {
news,
resid: resId,
source,
summary,
uniseq: id,
},
},
prompt,
ver: '0.0.0.5',
view: 'contact',
};
}
static fromResId(resId: string): ForwardMsgJson {
return this.build(resId, []);
}
static fromResId (resId: string): ForwardMsgJson {
return this.build(resId, []);
}
static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
}))
})), source, news, summary, prompt);
}
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
})),
})), source, news, summary, prompt);
}
}

View File

@@ -1,280 +1,280 @@
export interface ResourceConfig<T extends any[], R> {
/** 资源获取函数 */
resourceFn: (...args: T) => Promise<R>;
/** 失败后禁用时间(毫秒),默认 30 秒 */
disableTime?: number;
/** 最大重试次数,默认 3 次 */
maxRetries?: number;
/** 主动测试间隔(毫秒),默认 60 秒 */
healthCheckInterval?: number;
/** 最大健康检查失败次数,超过后永久禁用,默认 5 次 */
maxHealthCheckFailures?: number;
/** 健康检查函数,如果提供则优先使用此函数进行健康检查 */
healthCheckFn?: (...args: T) => Promise<boolean>;
/** 测试参数(用于健康检查) */
testArgs?: T;
/** 资源获取函数 */
resourceFn: (...args: T) => Promise<R>;
/** 失败后禁用时间(毫秒),默认 30 秒 */
disableTime?: number;
/** 最大重试次数,默认 3 次 */
maxRetries?: number;
/** 主动测试间隔(毫秒),默认 60 秒 */
healthCheckInterval?: number;
/** 最大健康检查失败次数,超过后永久禁用,默认 5 次 */
maxHealthCheckFailures?: number;
/** 健康检查函数,如果提供则优先使用此函数进行健康检查 */
healthCheckFn?: (...args: T) => Promise<boolean>;
/** 测试参数(用于健康检查) */
testArgs?: T;
}
interface ResourceTypeState {
/** 资源配置 */
config: {
resourceFn: (...args: any[]) => Promise<any>;
healthCheckFn?: (...args: any[]) => Promise<boolean>;
disableTime: number;
maxRetries: number;
healthCheckInterval: number;
maxHealthCheckFailures: number;
testArgs?: any[];
};
/** 是否启用 */
isEnabled: boolean;
/** 禁用截止时间 */
disableUntil: number;
/** 当前重试次数 */
currentRetries: number;
/** 健康检查失败次数 */
healthCheckFailureCount: number;
/** 是否永久禁用 */
isPermanentlyDisabled: boolean;
/** 上次健康检查时间 */
lastHealthCheckTime: number;
/** 成功次数统计 */
successCount: number;
/** 失败次数统计 */
failureCount: number;
/** 资源配置 */
config: {
resourceFn: (...args: any[]) => Promise<any>;
healthCheckFn?: (...args: any[]) => Promise<boolean>;
disableTime: number;
maxRetries: number;
healthCheckInterval: number;
maxHealthCheckFailures: number;
testArgs?: any[];
};
/** 是否启用 */
isEnabled: boolean;
/** 禁用截止时间 */
disableUntil: number;
/** 当前重试次数 */
currentRetries: number;
/** 健康检查失败次数 */
healthCheckFailureCount: number;
/** 是否永久禁用 */
isPermanentlyDisabled: boolean;
/** 上次健康检查时间 */
lastHealthCheckTime: number;
/** 成功次数统计 */
successCount: number;
/** 失败次数统计 */
failureCount: number;
}
export class ResourceManager {
private resourceTypes = new Map<string, ResourceTypeState>();
private destroyed = false;
private resourceTypes = new Map<string, ResourceTypeState>();
private destroyed = false;
/**
/**
* 调用资源(自动注册或复用已有配置)
*/
async callResource<T extends any[], R>(
type: string,
config: ResourceConfig<T, R>,
...args: T
): Promise<R> {
if (this.destroyed) {
throw new Error('ResourceManager has been destroyed');
}
// 获取或创建资源类型状态
let state = this.resourceTypes.get(type);
if (!state) {
// 首次注册
state = {
config: {
resourceFn: config.resourceFn as (...args: any[]) => Promise<any>,
healthCheckFn: config.healthCheckFn as ((...args: any[]) => Promise<boolean>) | undefined,
disableTime: config.disableTime ?? 30000,
maxRetries: config.maxRetries ?? 3,
healthCheckInterval: config.healthCheckInterval ?? 60000,
maxHealthCheckFailures: config.maxHealthCheckFailures ?? 20,
testArgs: config.testArgs as any[] | undefined,
},
isEnabled: true,
disableUntil: 0,
currentRetries: 0,
healthCheckFailureCount: 0,
isPermanentlyDisabled: false,
lastHealthCheckTime: 0,
successCount: 0,
failureCount: 0,
};
this.resourceTypes.set(type, state);
}
// 在调用前检查是否需要进行健康检查
await this.checkAndPerformHealthCheck(state);
// 检查资源状态
if (state.isPermanentlyDisabled) {
throw new Error(`Resource type '${type}' is permanently disabled (success: ${state.successCount}, failure: ${state.failureCount})`);
}
if (!this.isResourceAvailable(type)) {
const disableUntilDate = new Date(state.disableUntil).toISOString();
throw new Error(`Resource type '${type}' is currently disabled until ${disableUntilDate} (success: ${state.successCount}, failure: ${state.failureCount})`);
}
// 调用资源
try {
const result = await config.resourceFn(...args);
this.onResourceSuccess(state);
return result;
} catch (error) {
this.onResourceFailure(state);
throw error;
}
async callResource<T extends any[], R>(
type: string,
config: ResourceConfig<T, R>,
...args: T
): Promise<R> {
if (this.destroyed) {
throw new Error('ResourceManager has been destroyed');
}
/**
// 获取或创建资源类型状态
let state = this.resourceTypes.get(type);
if (!state) {
// 首次注册
state = {
config: {
resourceFn: config.resourceFn as (...args: any[]) => Promise<any>,
healthCheckFn: config.healthCheckFn as ((...args: any[]) => Promise<boolean>) | undefined,
disableTime: config.disableTime ?? 30000,
maxRetries: config.maxRetries ?? 3,
healthCheckInterval: config.healthCheckInterval ?? 60000,
maxHealthCheckFailures: config.maxHealthCheckFailures ?? 20,
testArgs: config.testArgs as any[] | undefined,
},
isEnabled: true,
disableUntil: 0,
currentRetries: 0,
healthCheckFailureCount: 0,
isPermanentlyDisabled: false,
lastHealthCheckTime: 0,
successCount: 0,
failureCount: 0,
};
this.resourceTypes.set(type, state);
}
// 在调用前检查是否需要进行健康检查
await this.checkAndPerformHealthCheck(state);
// 检查资源状态
if (state.isPermanentlyDisabled) {
throw new Error(`Resource type '${type}' is permanently disabled (success: ${state.successCount}, failure: ${state.failureCount})`);
}
if (!this.isResourceAvailable(type)) {
const disableUntilDate = new Date(state.disableUntil).toISOString();
throw new Error(`Resource type '${type}' is currently disabled until ${disableUntilDate} (success: ${state.successCount}, failure: ${state.failureCount})`);
}
// 调用资源
try {
const result = await config.resourceFn(...args);
this.onResourceSuccess(state);
return result;
} catch (error) {
this.onResourceFailure(state);
throw error;
}
}
/**
* 检查资源类型是否可用
*/
isResourceAvailable(type: string): boolean {
const state = this.resourceTypes.get(type);
if (!state) {
return true; // 未注册的资源类型视为可用
}
if (state.isPermanentlyDisabled || !state.isEnabled) {
return false;
}
return Date.now() >= state.disableUntil;
isResourceAvailable (type: string): boolean {
const state = this.resourceTypes.get(type);
if (!state) {
return true; // 未注册的资源类型视为可用
}
/**
if (state.isPermanentlyDisabled || !state.isEnabled) {
return false;
}
return Date.now() >= state.disableUntil;
}
/**
* 获取资源类型统计信息
*/
getResourceStats(type: string): { successCount: number; failureCount: number; isEnabled: boolean; isPermanentlyDisabled: boolean } | null {
const state = this.resourceTypes.get(type);
if (!state) {
return null;
}
return {
successCount: state.successCount,
failureCount: state.failureCount,
isEnabled: state.isEnabled,
isPermanentlyDisabled: state.isPermanentlyDisabled,
};
getResourceStats (type: string): { successCount: number; failureCount: number; isEnabled: boolean; isPermanentlyDisabled: boolean } | null {
const state = this.resourceTypes.get(type);
if (!state) {
return null;
}
/**
return {
successCount: state.successCount,
failureCount: state.failureCount,
isEnabled: state.isEnabled,
isPermanentlyDisabled: state.isPermanentlyDisabled,
};
}
/**
* 获取所有资源类型统计
*/
getAllResourceStats(): Map<string, { successCount: number; failureCount: number; isEnabled: boolean; isPermanentlyDisabled: boolean }> {
const stats = new Map();
for (const [type, state] of this.resourceTypes) {
stats.set(type, {
successCount: state.successCount,
failureCount: state.failureCount,
isEnabled: state.isEnabled,
isPermanentlyDisabled: state.isPermanentlyDisabled,
});
}
return stats;
getAllResourceStats (): Map<string, { successCount: number; failureCount: number; isEnabled: boolean; isPermanentlyDisabled: boolean }> {
const stats = new Map();
for (const [type, state] of this.resourceTypes) {
stats.set(type, {
successCount: state.successCount,
failureCount: state.failureCount,
isEnabled: state.isEnabled,
isPermanentlyDisabled: state.isPermanentlyDisabled,
});
}
return stats;
}
/**
/**
* 注销资源类型
*/
unregister(type: string): boolean {
return this.resourceTypes.delete(type);
}
unregister (type: string): boolean {
return this.resourceTypes.delete(type);
}
/**
/**
* 销毁管理器
*/
destroy(): void {
if (this.destroyed) {
return;
}
this.resourceTypes.clear();
this.destroyed = true;
destroy (): void {
if (this.destroyed) {
return;
}
/**
this.resourceTypes.clear();
this.destroyed = true;
}
/**
* 检查并执行健康检查(如果需要)
*/
private async checkAndPerformHealthCheck(state: ResourceTypeState): Promise<void> {
// 如果资源可用或已永久禁用,无需健康检查
if (state.isEnabled && Date.now() >= state.disableUntil) {
return;
}
if (state.isPermanentlyDisabled) {
return;
}
const now = Date.now();
// 检查是否还在禁用期内
if (now < state.disableUntil) {
return;
}
// 检查是否需要进行健康检查(根据间隔时间)
if (now - state.lastHealthCheckTime < state.config.healthCheckInterval) {
return;
}
// 执行健康检查
await this.performHealthCheck(state);
private async checkAndPerformHealthCheck (state: ResourceTypeState): Promise<void> {
// 如果资源可用或已永久禁用,无需健康检查
if (state.isEnabled && Date.now() >= state.disableUntil) {
return;
}
private async performHealthCheck(state: ResourceTypeState): Promise<void> {
state.lastHealthCheckTime = Date.now();
try {
let healthCheckResult: boolean;
if (state.config.healthCheckFn) {
const testArgs = state.config.testArgs || [];
healthCheckResult = await state.config.healthCheckFn(...testArgs);
} else {
const testArgs = state.config.testArgs || [];
await state.config.resourceFn(...testArgs);
healthCheckResult = true;
}
if (healthCheckResult) {
// 健康检查成功,重新启用
state.isEnabled = true;
state.disableUntil = 0;
state.currentRetries = 0;
state.healthCheckFailureCount = 0;
} else {
throw new Error('Health check function returned false');
}
} catch {
// 健康检查失败,增加失败计数
state.healthCheckFailureCount++;
// 检查是否达到最大健康检查失败次数
if (state.healthCheckFailureCount >= state.config.maxHealthCheckFailures) {
// 永久禁用资源
state.isPermanentlyDisabled = true;
state.disableUntil = 0;
} else {
// 继续禁用一段时间
state.disableUntil = Date.now() + state.config.disableTime;
}
}
if (state.isPermanentlyDisabled) {
return;
}
private onResourceSuccess(state: ResourceTypeState): void {
state.currentRetries = 0;
const now = Date.now();
// 检查是否还在禁用期内
if (now < state.disableUntil) {
return;
}
// 检查是否需要进行健康检查(根据间隔时间)
if (now - state.lastHealthCheckTime < state.config.healthCheckInterval) {
return;
}
// 执行健康检查
await this.performHealthCheck(state);
}
private async performHealthCheck (state: ResourceTypeState): Promise<void> {
state.lastHealthCheckTime = Date.now();
try {
let healthCheckResult: boolean;
if (state.config.healthCheckFn) {
const testArgs = state.config.testArgs || [];
healthCheckResult = await state.config.healthCheckFn(...testArgs);
} else {
const testArgs = state.config.testArgs || [];
await state.config.resourceFn(...testArgs);
healthCheckResult = true;
}
if (healthCheckResult) {
// 健康检查成功,重新启用
state.isEnabled = true;
state.disableUntil = 0;
state.currentRetries = 0;
state.healthCheckFailureCount = 0;
state.successCount++;
}
} else {
throw new Error('Health check function returned false');
}
} catch {
// 健康检查失败,增加失败计数
state.healthCheckFailureCount++;
private onResourceFailure(state: ResourceTypeState): void {
state.currentRetries++;
state.failureCount++;
// 如果重试次数达到上限,禁用资源
if (state.currentRetries >= state.config.maxRetries) {
state.disableUntil = Date.now() + state.config.disableTime;
state.currentRetries = 0;
}
// 检查是否达到最大健康检查失败次数
if (state.healthCheckFailureCount >= state.config.maxHealthCheckFailures) {
// 永久禁用资源
state.isPermanentlyDisabled = true;
state.disableUntil = 0;
} else {
// 继续禁用一段时间
state.disableUntil = Date.now() + state.config.disableTime;
}
}
}
private onResourceSuccess (state: ResourceTypeState): void {
state.currentRetries = 0;
state.disableUntil = 0;
state.healthCheckFailureCount = 0;
state.successCount++;
}
private onResourceFailure (state: ResourceTypeState): void {
state.currentRetries++;
state.failureCount++;
// 如果重试次数达到上限,禁用资源
if (state.currentRetries >= state.config.maxRetries) {
state.disableUntil = Date.now() + state.config.disableTime;
state.currentRetries = 0;
}
}
}
// 创建全局实例
export const resourceManager = new ResourceManager();
// 便捷函数
export async function registerResource<T extends any[], R>(
type: string,
config: ResourceConfig<T, R>,
...args: T
export async function registerResource<T extends any[], R> (
type: string,
config: ResourceConfig<T, R>,
...args: T
): Promise<R> {
return resourceManager.callResource(type, config, ...args);
}
return resourceManager.callResource(type, config, ...args);
}

View File

@@ -1,209 +1,208 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import path from 'node:path';
import fs from 'fs';
import os from 'node:os';
import { QQLevel } from '@/core';
import { QQVersionConfigType } from './types';
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
return new Promise<ReturnType<T> | undefined>((resolve) => {
try {
const result = func(...args);
resolve(result);
} catch {
resolve(undefined);
export async function solveProblem<T extends (...arg: any[]) => any> (func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
return new Promise<ReturnType<T> | undefined>((resolve) => {
try {
const result = func(...args);
resolve(result);
} catch {
resolve(undefined);
}
});
}
export async function solveAsyncProblem<T extends (...args: any[]) => Promise<any>> (func: T, ...args: Parameters<T>): Promise<Awaited<ReturnType<T>> | undefined> {
return new Promise<Awaited<ReturnType<T>> | undefined>((resolve) => {
func(...args).then((result) => {
resolve(result);
}).catch(() => {
resolve(undefined);
});
});
}
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>((_resolve, 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 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 async function solveAsyncProblem<T extends (...args: any[]) => Promise<any>>(func: T, ...args: Parameters<T>): Promise<Awaited<ReturnType<T>> | undefined> {
return new Promise<Awaited<ReturnType<T>> | undefined>((resolve) => {
func(...args).then((result) => {
resolve(result);
}).catch(() => {
resolve(undefined);
});
});
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 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 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 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.28060',
curVersion: '3.2.12.28060',
prevVersion: '',
onErrorVersions: [],
buildId: '27254',
};
}
if (os.platform() === 'darwin') {
return {
baseVersion: '6.9.53.28060',
curVersion: '6.9.53.28060',
prevVersion: '',
onErrorVersions: [],
buildId: '28060',
};
}
export function getDefaultQQVersionConfigInfo (): QQVersionConfigType {
if (os.platform() === 'linux') {
return {
baseVersion: '9.9.15-28131',
curVersion: '9.9.15-28131',
prevVersion: '',
onErrorVersions: [],
buildId: '28131',
baseVersion: '3.2.12.28060',
curVersion: '3.2.12.28060',
prevVersion: '',
onErrorVersions: [],
buildId: '27254',
};
}
if (os.platform() === 'darwin') {
return {
baseVersion: '6.9.53.28060',
curVersion: '6.9.53.28060',
prevVersion: '',
onErrorVersions: [],
buildId: '28060',
};
}
return {
baseVersion: '9.9.15-28131',
curVersion: '9.9.15-28131',
prevVersion: '',
onErrorVersions: [],
buildId: '28131',
};
}
export function getQQPackageInfoPath(exePath: string = '', version?: string): string {
let packagePath;
if (os.platform() === 'darwin') {
packagePath = path.join(path.dirname(exePath), '..', 'Resources', 'app', 'package.json');
} else if (os.platform() === 'linux') {
packagePath = path.join(path.dirname(exePath), './resources/app/package.json');
} else {
packagePath = path.join(path.dirname(exePath), './versions/' + version + '/resources/app/package.json');
}
//下面是老版本兼容 未来去掉
if (!fs.existsSync(packagePath)) {
packagePath = path.join(path.dirname(exePath), './resources/app/versions/' + version + '/package.json');
}
return packagePath;
export function getQQPackageInfoPath (exePath: string = '', version?: string): string {
let packagePath;
if (os.platform() === 'darwin') {
packagePath = path.join(path.dirname(exePath), '..', 'Resources', 'app', 'package.json');
} else if (os.platform() === 'linux') {
packagePath = path.join(path.dirname(exePath), './resources/app/package.json');
} else {
packagePath = path.join(path.dirname(exePath), './versions/' + version + '/resources/app/package.json');
}
// 下面是老版本兼容 未来去掉
if (!fs.existsSync(packagePath)) {
packagePath = path.join(path.dirname(exePath), './resources/app/versions/' + version + '/package.json');
}
return packagePath;
}
export function getQQVersionConfigPath(exePath: string = ''): string | undefined {
let configVersionInfoPath;
if (os.platform() === 'win32') {
configVersionInfoPath = path.join(path.dirname(exePath), 'versions', 'config.json');
} else if (os.platform() === 'darwin') {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
configVersionInfoPath = path.resolve(appDataPath, './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)) {
configVersionInfoPath = path.join(path.dirname(exePath), './resources/app/versions/config.json');
}
if (!fs.existsSync(configVersionInfoPath)) {
return undefined;
}
return configVersionInfoPath;
}
export function calcQQLevel(level?: QQLevel) {
if (!level) return 0;
//const { penguinNum, crownNum, sunNum, moonNum, starNum } = level;
const { crownNum, sunNum, moonNum, starNum } = level
//没补类型
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
}
export function stringifyWithBigInt(obj: any) {
return JSON.stringify(obj, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
}
export function parseAppidFromMajor(nodeMajor: string): string | undefined {
const hexSequence = 'A4 09 00 00 00 35';
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ''), 'hex');
const filePath = path.resolve(nodeMajor);
const fileContent = fs.readFileSync(filePath);
let searchPosition = 0;
while (true) {
const index = fileContent.indexOf(sequenceBytes, searchPosition);
if (index === -1) {
break;
}
const start = index + sequenceBytes.length - 1;
const end = fileContent.indexOf(0x00, start);
if (end === -1) {
break;
}
const content = fileContent.subarray(start, end);
if (!content.every(byte => byte === 0x00)) {
try {
return content.toString('utf-8');
} catch {
break;
}
}
searchPosition = end + 1;
}
export function getQQVersionConfigPath (exePath: string = ''): string | undefined {
let configVersionInfoPath;
if (os.platform() === 'win32') {
configVersionInfoPath = path.join(path.dirname(exePath), 'versions', 'config.json');
} else if (os.platform() === 'darwin') {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
configVersionInfoPath = path.resolve(appDataPath, './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)) {
configVersionInfoPath = path.join(path.dirname(exePath), './resources/app/versions/config.json');
}
if (!fs.existsSync(configVersionInfoPath)) {
return undefined;
}
return configVersionInfoPath;
}
export function calcQQLevel (level?: QQLevel) {
if (!level) return 0;
// const { penguinNum, crownNum, sunNum, moonNum, starNum } = level;
const { crownNum, sunNum, moonNum, starNum } = level;
// 没补类型
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
}
export function stringifyWithBigInt (obj: any) {
return JSON.stringify(obj, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
}
export function parseAppidFromMajor (nodeMajor: string): string | undefined {
const hexSequence = 'A4 09 00 00 00 35';
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ''), 'hex');
const filePath = path.resolve(nodeMajor);
const fileContent = fs.readFileSync(filePath);
let searchPosition = 0;
while (true) {
const index = fileContent.indexOf(sequenceBytes, searchPosition);
if (index === -1) {
break;
}
const start = index + sequenceBytes.length - 1;
const end = fileContent.indexOf(0x00, start);
if (end === -1) {
break;
}
const content = fileContent.subarray(start, end);
if (!content.every(byte => byte === 0x00)) {
try {
return content.toString('utf-8');
} catch {
break;
}
}
searchPosition = end + 1;
}
return undefined;
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import winston, { format, transports } from 'winston';
import { truncateString } from '@/common/helper';
import path from 'node:path';
@@ -6,315 +5,317 @@ import fs from 'node:fs/promises';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
import EventEmitter from 'node:events';
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
FATAL = 'fatal',
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
FATAL = 'fatal',
}
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}`;
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 logEmitter = new EventEmitter();
export type LogListener = (msg: string) => void;
class Subscription {
public static MAX_HISTORY = 100;
public static history: string[] = [];
public static MAX_HISTORY = 100;
public static history: string[] = [];
subscribe(listener: LogListener) {
for (const history of Subscription.history) {
try {
listener(history);
} catch {
// ignore
}
}
logEmitter.on('log', listener);
subscribe (listener: LogListener) {
for (const history of Subscription.history) {
try {
listener(history);
} catch {
// ignore
}
}
unsubscribe(listener: LogListener) {
logEmitter.off('log', listener);
}
notify(msg: string) {
logEmitter.emit('log', msg);
if (Subscription.history.length >= Subscription.MAX_HISTORY) {
Subscription.history.shift();
}
Subscription.history.push(msg);
logEmitter.on('log', listener);
}
unsubscribe (listener: LogListener) {
logEmitter.off('log', listener);
}
notify (msg: string) {
logEmitter.emit('log', msg);
if (Subscription.history.length >= Subscription.MAX_HISTORY) {
Subscription.history.shift();
}
Subscription.history.push(msg);
}
}
export const logSubscription = new Subscription();
export class LogWrapper {
fileLogEnabled = true;
consoleLogEnabled = true;
logger: winston.Logger;
fileLogEnabled = true;
consoleLogEnabled = true;
logger: winston.Logger;
constructor(logDir: string) {
const filename = `${getFormattedTimestamp()}.log`;
const logPath = path.join(logDir, filename);
constructor (logDir: string) {
const filename = `${getFormattedTimestamp()}.log`;
const logPath = path.join(logDir, filename);
this.logger = winston.createLogger({
level: 'debug',
format: format.combine(
format.timestamp({ format: 'MM-DD HH:mm:ss' }),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta['userInfo'] ? `${meta['userInfo']} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
),
transports: [
new transports.File({
filename: logPath,
level: 'debug',
maxsize: 5 * 1024 * 1024, // 5MB
maxFiles: 5,
}),
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta['userInfo'] ? `${meta['userInfo']} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
),
}),
],
});
this.setLogSelfInfo({ nick: '', uid: '' });
this.cleanOldLogs(logDir);
}
cleanOldLogs(logDir: string) {
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
fs.readdir(logDir).then((files) => {
files.forEach((file) => {
const filePath = path.join(logDir, file);
this.deleteOldLogFile(filePath, oneWeekAgo);
});
}).catch((err) => {
this.logger.error('Failed to read log directory', err);
});
}
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
fs.stat(filePath).then((stats) => {
if (stats.mtime.getTime() < oneWeekAgo) {
fs.unlink(filePath).catch((err) => {
if (err) {
if (err.code === 'ENOENT') {
this.logger.warn(`File already deleted: ${filePath}`);
} else {
this.logger.error('Failed to delete old log file', err);
}
} else {
this.logger.info(`Deleted old log file: ${filePath}`);
}
});
}
}).catch((err) => {
this.logger.error('Failed to get file stats', err);
});
}
setFileAndConsoleLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) {
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.File) {
transport.level = fileLogLevel;
} else if (transport instanceof transports.Console) {
transport.level = consoleLogLevel;
}
});
}
setLogSelfInfo(selfInfo: { nick: string; uid: string }) {
const userInfo = `${selfInfo.nick}`;
this.logger.defaultMeta = { userInfo };
}
setFileLogEnabled(isEnabled: boolean) {
this.fileLogEnabled = isEnabled;
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.File) {
transport.silent = !isEnabled;
}
});
}
setConsoleLogEnabled(isEnabled: boolean) {
this.consoleLogEnabled = isEnabled;
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.Console) {
transport.silent = !isEnabled;
}
});
}
formatMsg(msg: any[]) {
return msg
.map((msgItem) => {
if (msgItem instanceof Error) {
return msgItem.stack;
} else if (typeof msgItem === 'object') {
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
}
return msgItem;
this.logger = winston.createLogger({
level: 'debug',
format: format.combine(
format.timestamp({ format: 'MM-DD HH:mm:ss' }),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta['userInfo'] ? `${meta['userInfo']} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
),
transports: [
new transports.File({
filename: logPath,
level: 'debug',
maxsize: 5 * 1024 * 1024, // 5MB
maxFiles: 5,
}),
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta['userInfo'] ? `${meta['userInfo']} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
.join(' ');
}
),
}),
],
});
_log(level: LogLevel, ...args: any[]) {
const message = this.formatMsg(args);
if (this.consoleLogEnabled && this.fileLogEnabled) {
this.logger.log(level, message);
} else if (this.consoleLogEnabled) {
this.logger.log(level, message);
} else if (this.fileLogEnabled) {
// eslint-disable-next-line no-control-regex
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
this.setLogSelfInfo({ nick: '', uid: '' });
this.cleanOldLogs(logDir);
}
cleanOldLogs (logDir: string) {
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
fs.readdir(logDir).then((files) => {
files.forEach((file) => {
const filePath = path.join(logDir, file);
this.deleteOldLogFile(filePath, oneWeekAgo);
});
}).catch((err) => {
this.logger.error('Failed to read log directory', err);
});
}
private deleteOldLogFile (filePath: string, oneWeekAgo: number) {
fs.stat(filePath).then((stats) => {
if (stats.mtime.getTime() < oneWeekAgo) {
fs.unlink(filePath).catch((err) => {
if (err) {
if (err.code === 'ENOENT') {
this.logger.warn(`File already deleted: ${filePath}`);
} else {
this.logger.error('Failed to delete old log file', err);
}
} else {
this.logger.info(`Deleted old log file: ${filePath}`);
}
});
}
}).catch((err) => {
this.logger.error('Failed to get file stats', err);
});
}
setFileAndConsoleLogLevel (fileLogLevel: LogLevel, consoleLogLevel: LogLevel) {
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.File) {
transport.level = fileLogLevel;
} else if (transport instanceof transports.Console) {
transport.level = consoleLogLevel;
}
});
}
setLogSelfInfo (selfInfo: { nick: string; uid: string; }) {
const userInfo = `${selfInfo.nick}`;
this.logger.defaultMeta = { userInfo };
}
setFileLogEnabled (isEnabled: boolean) {
this.fileLogEnabled = isEnabled;
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.File) {
transport.silent = !isEnabled;
}
});
}
setConsoleLogEnabled (isEnabled: boolean) {
this.consoleLogEnabled = isEnabled;
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.Console) {
transport.silent = !isEnabled;
}
});
}
formatMsg (msg: any[]) {
return msg
.map((msgItem) => {
if (msgItem instanceof Error) {
return msgItem.stack;
} else if (typeof msgItem === 'object') {
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
}
logSubscription.notify(JSON.stringify({ level, message }));
return msgItem;
})
.join(' ');
}
_log (level: LogLevel, ...args: any[]) {
const message = this.formatMsg(args);
if (this.consoleLogEnabled && this.fileLogEnabled) {
this.logger.log(level, message);
} else if (this.consoleLogEnabled) {
this.logger.log(level, message);
} else if (this.fileLogEnabled) {
// eslint-disable-next-line no-control-regex
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
}
logSubscription.notify(JSON.stringify({ level, message }));
}
log (...args: any[]) {
this._log(LogLevel.INFO, ...args);
}
logDebug (...args: any[]) {
this._log(LogLevel.DEBUG, ...args);
}
logError (...args: any[]) {
this._log(LogLevel.ERROR, ...args);
}
logWarn (...args: any[]) {
this._log(LogLevel.WARN, ...args);
}
logFatal (...args: any[]) {
this._log(LogLevel.FATAL, ...args);
}
logMessage (msg: RawMessage, selfInfo: SelfInfo) {
const isSelfSent = msg.senderUin === selfInfo.uin;
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
return;
}
log(...args: any[]) {
this._log(LogLevel.INFO, ...args);
}
logDebug(...args: any[]) {
this._log(LogLevel.DEBUG, ...args);
}
logError(...args: any[]) {
this._log(LogLevel.ERROR, ...args);
}
logWarn(...args: any[]) {
this._log(LogLevel.WARN, ...args);
}
logFatal(...args: any[]) {
this._log(LogLevel.FATAL, ...args);
}
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
const isSelfSent = msg.senderUin === selfInfo.uin;
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
return;
}
this.log(`${isSelfSent ? '发送 ->' : '接收 <-'} ${rawMessageToText(msg)}`);
}
this.log(`${isSelfSent ? '发送 ->' : '接收 <-'} ${rawMessageToText(msg)}`);
}
}
export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
if (recursiveLevel > 2) {
return '...';
export function rawMessageToText (msg: RawMessage, recursiveLevel = 0): string {
if (recursiveLevel > 2) {
return '...';
}
const tokens: string[] = [];
if (msg.chatType === ChatType.KCHATTYPEC2C) {
tokens.push(`私聊 (${msg.peerUin})`);
} else if (msg.chatType === ChatType.KCHATTYPEGROUP) {
if (recursiveLevel < 1) {
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
}
const tokens: string[] = [];
if (msg.chatType == ChatType.KCHATTYPEC2C) {
tokens.push(`私聊 (${msg.peerUin})`);
} else if (msg.chatType == ChatType.KCHATTYPEGROUP) {
if (recursiveLevel < 1) {
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
}
if (msg.senderUin !== '0') {
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
}
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
tokens.push('移动设备');
} else {
tokens.push(`临时消息 (${msg.peerUin})`);
if (msg.senderUin !== '0') {
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
}
} else if (msg.chatType === ChatType.KCHATTYPEDATALINE) {
tokens.push('移动设备');
} else {
tokens.push(`临时消息 (${msg.peerUin})`);
}
for (const element of msg.elements) {
tokens.push(msgElementToText(element, msg, recursiveLevel));
}
for (const element of msg.elements) {
tokens.push(msgElementToText(element, msg, recursiveLevel));
}
return tokens.join(' ');
return tokens.join(' ');
}
function msgElementToText(element: MessageElement, msg: RawMessage, recursiveLevel: number): string {
if (element.textElement) {
return textElementToText(element.textElement);
}
function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLevel: number): string {
if (element.textElement) {
return textElementToText(element.textElement);
}
if (element.replyElement) {
return replyElementToText(element.replyElement, msg, recursiveLevel);
}
if (element.replyElement) {
return replyElementToText(element.replyElement, msg, recursiveLevel);
}
if (element.picElement) {
return '[图片]';
}
if (element.picElement) {
return '[图片]';
}
if (element.fileElement) {
return `[文件 ${element.fileElement.fileName}]`;
}
if (element.fileElement) {
return `[文件 ${element.fileElement.fileName}]`;
}
if (element.videoElement) {
return '[视频]';
}
if (element.videoElement) {
return '[视频]';
}
if (element.pttElement) {
return `[语音 ${element.pttElement.duration}s]`;
}
if (element.pttElement) {
return `[语音 ${element.pttElement.duration}s]`;
}
if (element.arkElement) {
return '[卡片消息]';
}
if (element.arkElement) {
return '[卡片消息]';
}
if (element.faceElement) {
return `[表情 ${element.faceElement.faceText ?? ''}]`;
}
if (element.faceElement) {
return `[表情 ${element.faceElement.faceText ?? ''}]`;
}
if (element.marketFaceElement) {
return element.marketFaceElement.faceName;
}
if (element.marketFaceElement) {
return element.marketFaceElement.faceName;
}
if (element.markdownElement) {
return '[Markdown 消息]';
}
if (element.markdownElement) {
return '[Markdown 消息]';
}
if (element.multiForwardMsgElement) {
return '[转发消息]';
}
if (element.multiForwardMsgElement) {
return '[转发消息]';
}
if (element.elementType === ElementType.GreyTip) {
return '[灰条消息]';
}
if (element.elementType === ElementType.GreyTip) {
return '[灰条消息]';
}
return `[未实现 (ElementType = ${element.elementType})]`;
return `[未实现 (ElementType = ${element.elementType})]`;
}
function textElementToText(textElement: any): string {
if (textElement.atType === NTMsgAtType.ATTYPEUNKNOWN) {
const originalContentLines = textElement.content.split('\n');
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
} else if (textElement.atType === NTMsgAtType.ATTYPEALL) {
return '@全体成员';
} else if (textElement.atType === NTMsgAtType.ATTYPEONE) {
return `${textElement.content} (${textElement.atUid})`;
}
return '';
function textElementToText (textElement: any): string {
if (textElement.atType === NTMsgAtType.ATTYPEUNKNOWN) {
const originalContentLines = textElement.content.split('\n');
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
} else if (textElement.atType === NTMsgAtType.ATTYPEALL) {
return '@全体成员';
} else if (textElement.atType === NTMsgAtType.ATTYPEONE) {
return `${textElement.content} (${textElement.atUid})`;
}
return '';
}
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
const recordMsgOrNull = msg.records.find((record) => replyElement.sourceMsgIdInRecords === record.msgId);
return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
? rawMessageToText(recordMsgOrNull, recursiveLevel + 1)
: `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
function replyElementToText (replyElement: any, msg: RawMessage, recursiveLevel: number): string {
const recordMsgOrNull = msg.records.find((record) => replyElement.sourceMsgIdInRecords === record.msgId);
return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin !== '284840486' && recordMsgOrNull.peerUin !== '1094950020'
? rawMessageToText(recordMsgOrNull, recursiveLevel + 1)
: `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
}]`;
}

View File

@@ -1,42 +1,43 @@
export class LRUCache<K, V> {
private capacity: number;
public cache: Map<K, V>;
private capacity: number;
public cache: Map<K, V>;
constructor(capacity: number) {
this.capacity = capacity;
this.cache = new Map<K, V>();
}
constructor (capacity: number) {
this.capacity = capacity;
this.cache = new Map<K, V>();
}
public get(key: K): V | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
// Move the accessed key to the end to mark it as most recently used
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
public get (key: K): V | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
// Move the accessed key to the end to mark it as most recently used
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
public put(key: K, value: V): void {
if (this.cache.has(key)) {
// If the key already exists, move it to the end to mark it as most recently used
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// If the cache is full, remove the least recently used key (the first one in the map)
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, value);
public put (key: K, value: V): void {
if (this.cache.has(key)) {
// If the key already exists, move it to the end to mark it as most recently used
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// If the cache is full, remove the least recently used key (the first one in the map)
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
public resetCapacity(newCapacity: number): void {
this.capacity = newCapacity;
while (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, value);
}
public resetCapacity (newCapacity: number): void {
this.capacity = newCapacity;
while (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
}
}
}

View File

@@ -2,142 +2,141 @@ import { Peer } from '@/core';
import crypto from 'crypto';
export class LimitedHashTable<K, V> {
private readonly keyToValue: Map<K, V> = new Map();
private readonly valueToKey: Map<V, K> = new Map();
private maxSize: number;
private readonly keyToValue: Map<K, V> = new Map();
private readonly valueToKey: Map<V, K> = new Map();
private maxSize: number;
constructor(maxSize: number) {
this.maxSize = maxSize;
constructor (maxSize: number) {
this.maxSize = maxSize;
}
resize (count: number) {
this.maxSize = count;
}
set (key: K, value: V): void {
this.keyToValue.set(key, value);
this.valueToKey.set(value, key);
while (this.keyToValue.size !== this.valueToKey.size) {
this.keyToValue.clear();
this.valueToKey.clear();
}
resize(count: number) {
this.maxSize = count;
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value;
if (oldestKey !== undefined) {
this.valueToKey.delete(this.keyToValue.get(oldestKey) as V);
this.keyToValue.delete(oldestKey);
}
}
}
set(key: K, value: V): void {
this.keyToValue.set(key, value);
this.valueToKey.set(value, key);
while (this.keyToValue.size !== this.valueToKey.size) {
this.keyToValue.clear();
this.valueToKey.clear();
}
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value;
if (oldestKey !== undefined) {
this.valueToKey.delete(this.keyToValue.get(oldestKey) as V);
this.keyToValue.delete(oldestKey);
}
}
getValue (key: K): V | undefined {
return this.keyToValue.get(key);
}
getKey (value: V): K | undefined {
return this.valueToKey.get(value);
}
deleteByValue (value: V): void {
const key = this.valueToKey.get(value);
if (key !== undefined) {
this.keyToValue.delete(key);
this.valueToKey.delete(value);
}
}
getValue(key: K): V | undefined {
return this.keyToValue.get(key);
deleteByKey (key: K): void {
const value = this.keyToValue.get(key);
if (value !== undefined) {
this.keyToValue.delete(key);
this.valueToKey.delete(value);
}
}
getKey(value: V): K | undefined {
return this.valueToKey.get(value);
getKeyList (): K[] {
return Array.from(this.keyToValue.keys());
}
// 获取最近刚写入的几个值
getHeads (size: number): { key: K; value: V }[] | undefined {
const keyList = this.getKeyList();
if (keyList.length === 0) {
return undefined;
}
deleteByValue(value: V): void {
const key = this.valueToKey.get(value);
if (key !== undefined) {
this.keyToValue.delete(key);
this.valueToKey.delete(value);
}
}
deleteByKey(key: K): void {
const value = this.keyToValue.get(key);
if (value !== undefined) {
this.keyToValue.delete(key);
this.valueToKey.delete(value);
}
}
getKeyList(): K[] {
return Array.from(this.keyToValue.keys());
}
//获取最近刚写入的几个值
getHeads(size: number): { key: K; value: V }[] | undefined {
const keyList = this.getKeyList();
if (keyList.length === 0) {
return undefined;
}
const result: { key: K; value: V }[] = [];
const listSize = Math.min(size, keyList.length);
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i];
if (key !== undefined) {
result.push({ key, value: this.keyToValue.get(key)! });
}
}
return result;
const result: { key: K; value: V }[] = [];
const listSize = Math.min(size, keyList.length);
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i];
if (key !== undefined) {
result.push({ key, value: this.keyToValue.get(key)! });
}
}
return result;
}
}
class MessageUniqueWrapper {
private readonly msgDataMap: LimitedHashTable<string, number>;
private readonly msgIdMap: LimitedHashTable<string, number>;
private readonly msgDataMap: LimitedHashTable<string, number>;
private readonly msgIdMap: LimitedHashTable<string, number>;
constructor(maxMap: number = 5000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
}
constructor (maxMap: number = 5000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
}
getRecentMsgIds(Peer: Peer, size: number): string[] {
const heads = this.msgIdMap.getHeads(size);
if (!heads) {
return [];
}
const data = heads.map((t) => MessageUnique.getMsgIdAndPeerByShortId(t.value));
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid);
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined);
getRecentMsgIds (Peer: Peer, size: number): string[] {
const heads = this.msgIdMap.getHeads(size);
if (!heads) {
return [];
}
const data = heads.map((t) => MessageUnique.getMsgIdAndPeerByShortId(t.value));
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid);
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined);
}
createUniqueMsgId(peer: Peer, msgId: string) {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
const hash = crypto.createHash('md5').update(key).digest();
if (hash[0]) {
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f;
}
const shortId = hash.readInt32BE(0);
//减少性能损耗
this.msgIdMap.set(msgId, shortId);
this.msgDataMap.set(key, shortId);
return shortId;
createUniqueMsgId (peer: Peer, msgId: string) {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
const hash = crypto.createHash('md5').update(key).digest();
if (hash[0]) {
// 设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f;
}
const shortId = hash.readInt32BE(0);
// 减少性能损耗
this.msgIdMap.set(msgId, shortId);
this.msgDataMap.set(key, shortId);
return shortId;
}
getMsgIdAndPeerByShortId(shortId: number): { MsgId: string; Peer: Peer } | undefined {
const data = this.msgDataMap.getKey(shortId);
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|');
const peer: Peer = {
chatType: parseInt(chatTypeStr ?? '0'),
peerUid: peerUid ?? '',
guildId: '',
};
return { MsgId: msgId ?? '0', Peer: peer };
}
return undefined;
getMsgIdAndPeerByShortId (shortId: number): { MsgId: string; Peer: Peer } | undefined {
const data = this.msgDataMap.getKey(shortId);
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|');
const peer: Peer = {
chatType: parseInt(chatTypeStr ?? '0'),
peerUid: peerUid ?? '',
guildId: '',
};
return { MsgId: msgId ?? '0', Peer: peer };
}
return undefined;
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId);
}
getShortIdByMsgId (msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId);
}
getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId);
if (!shortId) return undefined;
return this.getMsgIdAndPeerByShortId(shortId);
}
getPeerByMsgId (msgId: string) {
const shortId = this.msgIdMap.getValue(msgId);
if (!shortId) return undefined;
return this.getMsgIdAndPeerByShortId(shortId);
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize);
this.msgDataMap.resize(maxSize);
}
resize (maxSize: number): void {
this.msgIdMap.resize(maxSize);
this.msgDataMap.resize(maxSize);
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();

View File

@@ -4,38 +4,38 @@ import fs from 'fs';
import os from 'os';
export class NapCatPathWrapper {
binaryPath: string;
logsPath: string;
configPath: string;
cachePath: string;
staticPath: string;
pluginPath: string;
binaryPath: string;
logsPath: string;
configPath: string;
cachePath: string;
staticPath: string;
pluginPath: string;
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
this.binaryPath = mainPath;
let writePath: string;
constructor (mainPath: string = dirname(fileURLToPath(import.meta.url))) {
this.binaryPath = mainPath;
let writePath: string;
if (process.env['NAPCAT_WORKDIR']) {
writePath = process.env['NAPCAT_WORKDIR'];
} else if (os.platform() === 'darwin') {
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
} else {
writePath = this.binaryPath;
}
this.logsPath = path.join(writePath, 'logs');
this.configPath = path.join(writePath, 'config');
this.pluginPath = path.join(writePath, 'plugins');//dynamic load
this.cachePath = path.join(writePath, 'cache');
this.staticPath = path.join(this.binaryPath, 'static');
if (!fs.existsSync(this.logsPath)) {
fs.mkdirSync(this.logsPath, { recursive: true });
}
if (!fs.existsSync(this.configPath)) {
fs.mkdirSync(this.configPath, { recursive: true });
}
if (!fs.existsSync(this.cachePath)) {
fs.mkdirSync(this.cachePath, { recursive: true });
}
if (process.env['NAPCAT_WORKDIR']) {
writePath = process.env['NAPCAT_WORKDIR'];
} else if (os.platform() === 'darwin') {
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
} else {
writePath = this.binaryPath;
}
this.logsPath = path.join(writePath, 'logs');
this.configPath = path.join(writePath, 'config');
this.pluginPath = path.join(writePath, 'plugins');// dynamic load
this.cachePath = path.join(writePath, 'cache');
this.staticPath = path.join(this.binaryPath, 'static');
if (!fs.existsSync(this.logsPath)) {
fs.mkdirSync(this.logsPath, { recursive: true });
}
if (!fs.existsSync(this.configPath)) {
fs.mkdirSync(this.configPath, { recursive: true });
}
if (!fs.existsSync(this.cachePath)) {
fs.mkdirSync(this.cachePath, { recursive: true });
}
}
}

View File

@@ -6,290 +6,291 @@ import * as fs from 'fs';
import * as path from 'path';
export interface FunctionStats {
name: string;
callCount: number;
totalTime: number;
averageTime: number;
minTime: number;
maxTime: number;
fileName?: string;
lineNumber?: number;
name: string;
callCount: number;
totalTime: number;
averageTime: number;
minTime: number;
maxTime: number;
fileName?: string;
lineNumber?: number;
}
export class PerformanceMonitor {
private static instance: PerformanceMonitor;
private stats = new Map<string, FunctionStats>();
private startTimes = new Map<string, number>();
private reportInterval: NodeJS.Timeout | null = null;
private static instance: PerformanceMonitor;
private stats = new Map<string, FunctionStats>();
private startTimes = new Map<string, number>();
private reportInterval: NodeJS.Timeout | null = null;
static getInstance(): PerformanceMonitor {
if (!PerformanceMonitor.instance) {
PerformanceMonitor.instance = new PerformanceMonitor();
// 启动定时统计报告
PerformanceMonitor.instance.startPeriodicReport();
}
return PerformanceMonitor.instance;
static getInstance (): PerformanceMonitor {
if (!PerformanceMonitor.instance) {
PerformanceMonitor.instance = new PerformanceMonitor();
// 启动定时统计报告
PerformanceMonitor.instance.startPeriodicReport();
}
return PerformanceMonitor.instance;
}
/**
/**
* 开始定时统计报告 (每60秒)
*/
private startPeriodicReport(): void {
if (this.reportInterval) {
clearInterval(this.reportInterval);
}
this.reportInterval = setInterval(() => {
if (this.stats.size > 0) {
this.printPeriodicReport();
this.writeDetailedLogToFile();
}
}, 60000); // 60秒
private startPeriodicReport (): void {
if (this.reportInterval) {
clearInterval(this.reportInterval);
}
/**
this.reportInterval = setInterval(() => {
if (this.stats.size > 0) {
this.printPeriodicReport();
this.writeDetailedLogToFile();
}
}, 60000); // 60秒
}
/**
* 停止定时统计报告
*/
stopPeriodicReport(): void {
if (this.reportInterval) {
clearInterval(this.reportInterval);
this.reportInterval = null;
}
stopPeriodicReport (): void {
if (this.reportInterval) {
clearInterval(this.reportInterval);
this.reportInterval = null;
}
}
/**
/**
* 打印定时统计报告 (简化版本)
*/
private printPeriodicReport(): void {
const now = new Date().toLocaleString();
console.log(`\n=== 性能监控定时报告 [${now}] ===`);
private printPeriodicReport (): void {
const now = new Date().toLocaleString();
console.log(`\n=== 性能监控定时报告 [${now}] ===`);
const totalFunctions = this.stats.size;
const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0);
const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0);
const totalFunctions = this.stats.size;
const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0);
const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0);
console.log(`📊 总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms`);
console.log(`📊 总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms`);
// 显示Top 5最活跃的函数
console.log('\n🔥 最活跃函数 (Top 5):');
this.getTopByCallCount(5).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms`);
});
// 显示Top 5最活跃的函数
console.log('\n🔥 最活跃函数 (Top 5):');
this.getTopByCallCount(5).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms`);
});
// 显示Top 5最耗时的函数
console.log('\n⏱ 最耗时函数 (Top 5):');
this.getTopByTotalTime(5).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms`);
});
// 显示Top 5最耗时的函数
console.log('\n⏱ 最耗时函数 (Top 5):');
this.getTopByTotalTime(5).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms`);
});
console.log('===============================\n');
}
console.log('===============================\n');
}
/**
/**
* 将详细统计数据写入日志文件
*/
private writeDetailedLogToFile(): void {
try {
const now = new Date();
const dateStr = now.toISOString().replace(/[:.]/g, '-').split('T')[0];
const timeStr = now.toTimeString().split(' ')[0]?.replace(/:/g, '-') || 'unknown-time';
const timestamp = `${dateStr}_${timeStr}`;
const fileName = `${timestamp}.log.txt`;
const logPath = path.join(process.cwd(), 'logs', fileName);
private writeDetailedLogToFile (): void {
try {
const now = new Date();
const dateStr = now.toISOString().replace(/[:.]/g, '-').split('T')[0];
const timeStr = now.toTimeString().split(' ')[0]?.replace(/:/g, '-') || 'unknown-time';
const timestamp = `${dateStr}_${timeStr}`;
const fileName = `${timestamp}.log.txt`;
const logPath = path.join(process.cwd(), 'logs', fileName);
// 确保logs目录存在
const logsDir = path.dirname(logPath);
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// 确保logs目录存在
const logsDir = path.dirname(logPath);
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const totalFunctions = this.stats.size;
const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0);
const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0);
const totalFunctions = this.stats.size;
const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0);
const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0);
let logContent = '';
logContent += `=== 性能监控详细报告 ===\n`;
logContent += `生成时间: ${now.toLocaleString()}\n`;
logContent += `统计周期: 60秒\n`;
logContent += `总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms\n\n`;
let logContent = '';
logContent += '=== 性能监控详细报告 ===\n';
logContent += `生成时间: ${now.toLocaleString()}\n`;
logContent += '统计周期: 60秒\n';
logContent += `总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms\n\n`;
// 详细函数统计
logContent += `=== 所有函数详细统计 ===\n`;
const allStats = this.getStats().sort((a, b) => b.totalTime - a.totalTime);
allStats.forEach((stat, index) => {
logContent += `${index + 1}. 函数: ${stat.name}\n`;
logContent += ` 文件: ${stat.fileName || 'N/A'}\n`;
logContent += ` 行号: ${stat.lineNumber || 'N/A'}\n`;
logContent += ` 调用次数: ${stat.callCount}\n`;
logContent += ` 总耗时: ${stat.totalTime.toFixed(4)}ms\n`;
logContent += ` 平均耗时: ${stat.averageTime.toFixed(4)}ms\n`;
logContent += ` 最小耗时: ${stat.minTime === Infinity ? 'N/A' : stat.minTime.toFixed(4)}ms\n`;
logContent += ` 最大耗时: ${stat.maxTime.toFixed(4)}ms\n`;
logContent += ` 性能占比: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`;
logContent += `\n`;
});
// 详细函数统计
logContent += '=== 所有函数详细统计 ===\n';
const allStats = this.getStats().sort((a, b) => b.totalTime - a.totalTime);
// 排行榜统计
logContent += `=== 总耗时排行榜 (Top 20) ===\n`;
this.getTopByTotalTime(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 平均: ${stat.averageTime.toFixed(2)}ms\n`;
});
allStats.forEach((stat, index) => {
logContent += `${index + 1}. 函数: ${stat.name}\n`;
logContent += ` 文件: ${stat.fileName || 'N/A'}\n`;
logContent += ` 行号: ${stat.lineNumber || 'N/A'}\n`;
logContent += ` 调用次数: ${stat.callCount}\n`;
logContent += ` 总耗时: ${stat.totalTime.toFixed(4)}ms\n`;
logContent += ` 平均耗时: ${stat.averageTime.toFixed(4)}ms\n`;
logContent += ` 最小耗时: ${stat.minTime === Infinity ? 'N/A' : stat.minTime.toFixed(4)}ms\n`;
logContent += ` 最大耗时: ${stat.maxTime.toFixed(4)}ms\n`;
logContent += ` 性能占比: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`;
logContent += '\n';
});
logContent += `\n=== 调用次数排行榜 (Top 20) ===\n`;
this.getTopByCallCount(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms\n`;
});
// 排行榜统计
logContent += '=== 总耗时排行榜 (Top 20) ===\n';
this.getTopByTotalTime(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 平均: ${stat.averageTime.toFixed(2)}ms\n`;
});
logContent += `\n=== 平均耗时排行榜 (Top 20) ===\n`;
this.getTopByAverageTime(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 平均: ${stat.averageTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms\n`;
});
logContent += '\n=== 调用次数排行榜 (Top 20) ===\n';
this.getTopByCallCount(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms\n`;
});
logContent += `\n=== 性能热点分析 ===\n`;
// 找出最耗时的前10个函数
const hotSpots = this.getTopByTotalTime(10);
hotSpots.forEach((stat, index) => {
const efficiency = stat.callCount / stat.totalTime; // 每毫秒的调用次数
logContent += `${index + 1}. ${stat.name}\n`;
logContent += ` 性能影响: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`;
logContent += ` 调用效率: ${efficiency.toFixed(4)} 调用/ms\n`;
logContent += ` 优化建议: ${stat.averageTime > 10 ? '考虑优化此函数的执行效率' :
stat.callCount > 1000 ? '考虑减少此函数的调用频率' :
'性能表现良好'}\n\n`;
});
logContent += '\n=== 平均耗时排行榜 (Top 20) ===\n';
this.getTopByAverageTime(20).forEach((stat, index) => {
logContent += `${index + 1}. ${stat.name} - 平均: ${stat.averageTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms\n`;
});
logContent += `=== 报告结束 ===\n`;
logContent += '\n=== 性能热点分析 ===\n';
// 找出最耗时的前10个函数
const hotSpots = this.getTopByTotalTime(10);
hotSpots.forEach((stat, index) => {
const efficiency = stat.callCount / stat.totalTime; // 每毫秒的调用次数
logContent += `${index + 1}. ${stat.name}\n`;
logContent += ` 性能影响: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`;
logContent += ` 调用效率: ${efficiency.toFixed(4)} 调用/ms\n`;
logContent += ` 优化建议: ${stat.averageTime > 10
? '考虑优化此函数的执行效率'
: stat.callCount > 1000
? '考虑减少此函数的调用频率'
: '性能表现良好'}\n\n`;
});
// 写入文件
fs.writeFileSync(logPath, logContent, 'utf8');
console.log(`📄 详细性能报告已保存到: ${logPath}`);
logContent += '=== 报告结束 ===\n';
} catch (error) {
console.error('写入性能日志文件时出错:', error);
}
// 写入文件
fs.writeFileSync(logPath, logContent, 'utf8');
console.log(`📄 详细性能报告已保存到: ${logPath}`);
} catch (error) {
console.error('写入性能日志文件时出错:', error);
}
}
/**
/**
* 开始记录函数调用
*/
startFunction(functionName: string, fileName?: string, lineNumber?: number): string {
const callId = `${functionName}_${Date.now()}_${Math.random()}`;
this.startTimes.set(callId, performance.now());
startFunction (functionName: string, fileName?: string, lineNumber?: number): string {
const callId = `${functionName}_${Date.now()}_${Math.random()}`;
this.startTimes.set(callId, performance.now());
// 初始化或更新统计信息
if (!this.stats.has(functionName)) {
this.stats.set(functionName, {
name: functionName,
callCount: 0,
totalTime: 0,
averageTime: 0,
minTime: Infinity,
maxTime: 0,
fileName,
lineNumber
});
}
const stat = this.stats.get(functionName)!;
stat.callCount++;
return callId;
// 初始化或更新统计信息
if (!this.stats.has(functionName)) {
this.stats.set(functionName, {
name: functionName,
callCount: 0,
totalTime: 0,
averageTime: 0,
minTime: Infinity,
maxTime: 0,
fileName,
lineNumber,
});
}
/**
const stat = this.stats.get(functionName)!;
stat.callCount++;
return callId;
}
/**
* 结束记录函数调用
*/
endFunction(callId: string, functionName: string): void {
const startTime = this.startTimes.get(callId);
if (!startTime) return;
endFunction (callId: string, functionName: string): void {
const startTime = this.startTimes.get(callId);
if (!startTime) return;
const endTime = performance.now();
const duration = endTime - startTime;
const endTime = performance.now();
const duration = endTime - startTime;
this.startTimes.delete(callId);
this.startTimes.delete(callId);
const stat = this.stats.get(functionName);
if (!stat) return;
const stat = this.stats.get(functionName);
if (!stat) return;
stat.totalTime += duration;
stat.averageTime = stat.totalTime / stat.callCount;
stat.minTime = Math.min(stat.minTime, duration);
stat.maxTime = Math.max(stat.maxTime, duration);
}
stat.totalTime += duration;
stat.averageTime = stat.totalTime / stat.callCount;
stat.minTime = Math.min(stat.minTime, duration);
stat.maxTime = Math.max(stat.maxTime, duration);
}
/**
/**
* 获取所有统计信息
*/
getStats(): FunctionStats[] {
return Array.from(this.stats.values());
}
getStats (): FunctionStats[] {
return Array.from(this.stats.values());
}
/**
/**
* 获取排行榜 - 按总耗时排序
*/
getTopByTotalTime(limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.totalTime - a.totalTime)
.slice(0, limit);
}
getTopByTotalTime (limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.totalTime - a.totalTime)
.slice(0, limit);
}
/**
/**
* 获取排行榜 - 按调用次数排序
*/
getTopByCallCount(limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.callCount - a.callCount)
.slice(0, limit);
}
getTopByCallCount (limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.callCount - a.callCount)
.slice(0, limit);
}
/**
/**
* 获取排行榜 - 按平均耗时排序
*/
getTopByAverageTime(limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.averageTime - a.averageTime)
.slice(0, limit);
}
getTopByAverageTime (limit = 20): FunctionStats[] {
return this.getStats()
.sort((a, b) => b.averageTime - a.averageTime)
.slice(0, limit);
}
/**
/**
* 清空统计数据
*/
clear(): void {
this.stats.clear();
this.startTimes.clear();
}
clear (): void {
this.stats.clear();
this.startTimes.clear();
}
/**
/**
* 打印统计报告
*/
printReport(): void {
console.log('\n=== 函数性能监控报告 ===');
printReport (): void {
console.log('\n=== 函数性能监控报告 ===');
console.log('\n🔥 总耗时排行榜 (Top 10):');
this.getTopByTotalTime(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 平均耗时: ${stat.averageTime.toFixed(2)}ms`);
});
console.log('\n🔥 总耗时排行榜 (Top 10):');
this.getTopByTotalTime(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 平均耗时: ${stat.averageTime.toFixed(2)}ms`);
});
console.log('\n📈 调用次数排行榜 (Top 10):');
this.getTopByCallCount(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均耗时: ${stat.averageTime.toFixed(2)}ms`);
});
console.log('\n📈 调用次数排行榜 (Top 10):');
this.getTopByCallCount(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均耗时: ${stat.averageTime.toFixed(2)}ms`);
});
console.log('\n⏱ 平均耗时排行榜 (Top 10):');
this.getTopByAverageTime(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 平均耗时: ${stat.averageTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms`);
});
console.log('\n⏱ 平均耗时排行榜 (Top 10):');
this.getTopByAverageTime(10).forEach((stat, index) => {
console.log(`${index + 1}. ${stat.name} - 平均耗时: ${stat.averageTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms`);
});
console.log('\n========================\n');
}
console.log('\n========================\n');
}
/**
/**
* 获取JSON格式的统计数据
*/
toJSON(): FunctionStats[] {
return this.getStats();
}
toJSON (): FunctionStats[] {
return this.getStats();
}
}
// 全局性能监控器实例
@@ -297,20 +298,20 @@ export const performanceMonitor = PerformanceMonitor.getInstance();
// 在进程退出时打印报告并停止定时器
if (typeof process !== 'undefined') {
process.on('exit', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
});
process.on('exit', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
});
process.on('SIGINT', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
process.exit(0);
});
process.on('SIGINT', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
process.exit(0);
});
process.on('SIGTERM', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
process.exit(0);
});
process.on('SIGTERM', () => {
performanceMonitor.stopPeriodicReport();
performanceMonitor.printReport();
process.exit(0);
});
}

View File

@@ -1,22 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { LogWrapper } from '@/common/log';
export function proxyHandlerOf(logger: LogWrapper) {
return {
get(target: any, prop: any, receiver: any) {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return (..._args: unknown[]) => {
logger.logDebug(`${target.constructor.name} has no method ${prop}`);
};
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver);
},
};
export function proxyHandlerOf (logger: LogWrapper) {
return {
get (target: any, prop: any, receiver: any) {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (..._args: unknown[]) => {
logger.logDebug(`${target.constructor.name} has no method ${prop}`);
};
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver);
},
};
}
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
return new Proxy<T>(listener, proxyHandlerOf(logger));
export function proxiedListenerOf<T extends object> (listener: T, logger: LogWrapper) {
return new Proxy<T>(listener, proxyHandlerOf(logger));
}

View File

@@ -7,101 +7,100 @@ import { getMajorPath } from '@/core';
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from './types';
export class QQBasicInfoWrapper {
QQMainPath: string | undefined;
QQPackageInfoPath: string | undefined;
QQVersionConfigPath: string | undefined;
isQuickUpdate: boolean | undefined;
QQVersionConfig: QQVersionConfigType | undefined;
QQPackageInfo: QQPackageInfoType | undefined;
QQVersionAppid: string | undefined;
QQVersionQua: string | undefined;
context: { logger: LogWrapper };
QQMainPath: string | undefined;
QQPackageInfoPath: string | undefined;
QQVersionConfigPath: string | undefined;
isQuickUpdate: boolean | undefined;
QQVersionConfig: QQVersionConfigType | undefined;
QQPackageInfo: QQPackageInfoType | undefined;
QQVersionAppid: string | undefined;
QQVersionQua: string | undefined;
context: { logger: LogWrapper; };
constructor(context: { logger: LogWrapper }) {
//基础目录获取
this.context = context;
this.QQMainPath = process.execPath;
this.QQVersionConfigPath = getQQVersionConfigPath(this.QQMainPath);
constructor (context: { logger: LogWrapper; }) {
// 基础目录获取
this.context = context;
this.QQMainPath = process.execPath;
this.QQVersionConfigPath = getQQVersionConfigPath(this.QQMainPath);
// 基础信息获取 无快更则启用默认模板填充
this.isQuickUpdate = !!this.QQVersionConfigPath;
this.QQVersionConfig = this.isQuickUpdate
? JSON.parse(fs.readFileSync(this.QQVersionConfigPath!).toString())
: getDefaultQQVersionConfigInfo();
//基础信息获取 无快更则启用默认模板填充
this.isQuickUpdate = !!this.QQVersionConfigPath;
this.QQVersionConfig = this.isQuickUpdate
? JSON.parse(fs.readFileSync(this.QQVersionConfigPath!).toString())
: getDefaultQQVersionConfigInfo();
this.QQPackageInfoPath = getQQPackageInfoPath(this.QQMainPath, this.QQVersionConfig?.curVersion);
this.QQPackageInfo = JSON.parse(fs.readFileSync(this.QQPackageInfoPath).toString());
const { appid: IQQVersionAppid, qua: IQQVersionQua } = this.getAppidV2();
this.QQVersionAppid = IQQVersionAppid;
this.QQVersionQua = IQQVersionQua;
}
this.QQPackageInfoPath = getQQPackageInfoPath(this.QQMainPath, this.QQVersionConfig?.curVersion);
this.QQPackageInfo = JSON.parse(fs.readFileSync(this.QQPackageInfoPath).toString());
const { appid: IQQVersionAppid, qua: IQQVersionQua } = this.getAppidV2();
this.QQVersionAppid = IQQVersionAppid;
this.QQVersionQua = IQQVersionQua;
// 基础函数
getQQBuildStr () {
return this.QQVersionConfig?.curVersion.split('-')[1] ?? this.QQPackageInfo?.buildVersion;
}
getFullQQVersion () {
const version = this.isQuickUpdate ? this.QQVersionConfig?.curVersion : this.QQPackageInfo?.version;
if (!version) throw new Error('QQ版本获取失败');
return version;
}
requireMinNTQQBuild (buildStr: string) {
const currentBuild = +(this.getQQBuildStr() ?? '0');
if (currentBuild === 0) throw new Error('QQBuildStr获取失败');
return currentBuild >= parseInt(buildStr);
}
// 此方法不要直接使用
getQUAFallback () {
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
win32: `V1_WIN_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
darwin: `V1_MAC_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
linux: `V1_LNX_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
};
return platformMapping[systemPlatform] ?? (platformMapping.win32)!;
}
getAppIdFallback () {
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
win32: '537246092',
darwin: '537246140',
linux: '537246140',
};
return platformMapping[systemPlatform] ?? '537246092';
}
getAppidV2 (): { appid: string; qua: string; } {
// 通过已有表 性能好
const appidTbale = AppidTable as unknown as QQAppidTableType;
const fullVersion = this.getFullQQVersion();
if (fullVersion) {
const data = appidTbale[fullVersion];
if (data) {
return data;
}
}
//基础函数
getQQBuildStr() {
return this.QQVersionConfig?.curVersion.split('-')[1] ?? this.QQPackageInfo?.buildVersion;
}
getFullQQVersion() {
const version = this.isQuickUpdate ? this.QQVersionConfig?.curVersion : this.QQPackageInfo?.version;
if (!version) throw new Error('QQ版本获取失败');
return version;
}
requireMinNTQQBuild(buildStr: string) {
const currentBuild = +(this.getQQBuildStr() ?? '0');
if (currentBuild == 0) throw new Error('QQBuildStr获取失败');
return currentBuild >= parseInt(buildStr);
}
//此方法不要直接使用
getQUAFallback() {
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
win32: `V1_WIN_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
darwin: `V1_MAC_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
linux: `V1_LNX_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`,
};
return platformMapping[systemPlatform] ?? (platformMapping.win32)!;
}
getAppIdFallback() {
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
win32: '537246092',
darwin: '537246140',
linux: '537246140',
};
return platformMapping[systemPlatform] ?? '537246092';
}
getAppidV2(): { appid: string; qua: string } {
// 通过已有表 性能好
const appidTbale = AppidTable as unknown as QQAppidTableType;
const fullVersion = this.getFullQQVersion();
if (fullVersion) {
const data = appidTbale[fullVersion];
if (data) {
return data;
}
}
// 通过Major拉取 性能差
try {
const majorAppid = this.getAppidV2ByMajor(fullVersion);
if (majorAppid) {
this.context.logger.log('[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat');
return { appid: majorAppid, qua: this.getQUAFallback() };
}
} catch {
this.context.logger.log('[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常');
}
// 最终兜底为老版本
this.context.logger.log('[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常');
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
}
getAppidV2ByMajor(QQVersion: string) {
const majorPath = getMajorPath(QQVersion);
const appid = parseAppidFromMajor(majorPath);
return appid;
// 通过Major拉取 性能差
try {
const majorAppid = this.getAppidV2ByMajor(fullVersion);
if (majorAppid) {
this.context.logger.log('[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat');
return { appid: majorAppid, qua: this.getQUAFallback() };
}
} catch {
this.context.logger.log('[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常');
}
// 最终兜底为老版本
this.context.logger.log('[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常');
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`);
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
}
getAppidV2ByMajor (QQVersion: string) {
const majorPath = getMajorPath(QQVersion);
const appid = parseAppidFromMajor(majorPath);
return appid;
}
}

View File

@@ -1,117 +1,115 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import https from 'node:https';
import http from 'node:http';
export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
const req = client.get(url, (res) => {
const cookies: { [key: string]: string } = {};
// 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies (url: string): Promise<{ [key: string]: string }> {
const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
const req = client.get(url, (res) => {
const cookies: { [key: string]: string } = {};
res.on('data', () => { }); // Necessary to consume the stream
res.on('end', () => {
this.handleRedirect(res, url, cookies)
.then(resolve)
.catch(reject);
});
if (res.headers['set-cookie']) {
this.extractCookies(res.headers['set-cookie'], cookies);
}
});
req.on('error', (error: Error) => {
reject(error);
});
res.on('data', () => { }); // Necessary to consume the stream
res.on('end', () => {
this.handleRedirect(res, url, cookies)
.then(resolve)
.catch(reject);
});
}
private static async handleRedirect(res: http.IncomingMessage, url: string, cookies: { [key: string]: string }): Promise<{ [key: string]: string }> {
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
const redirectCookies = await this.HttpsGetCookies(redirectUrl.href);
// 合并重定向过程中的cookies
return { ...cookies, ...redirectCookies };
}
if (res.headers['set-cookie']) {
this.extractCookies(res.headers['set-cookie'], cookies);
}
return cookies;
}
});
private static extractCookies(setCookieHeaders: string[], cookies: { [key: string]: string }) {
setCookieHeaders.forEach((cookie) => {
const parts = cookie.split(';')[0]?.split('=');
if (parts) {
const key = parts[0];
const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
}
}
req.on('error', (error: Error) => {
reject(error);
});
});
}
private static async handleRedirect (res: http.IncomingMessage, url: string, cookies: { [key: string]: string }): Promise<{ [key: string]: string }> {
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
const redirectCookies = await this.HttpsGetCookies(redirectUrl.href);
// 合并重定向过程中的cookies
return { ...cookies, ...redirectCookies };
}
}
return cookies;
}
private static extractCookies (setCookieHeaders: string[], cookies: { [key: string]: string }) {
setCookieHeaders.forEach((cookie) => {
const parts = cookie.split(';')[0]?.split('=');
if (parts) {
const key = parts[0];
const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
}
}
});
}
// 请求和回复都是JSON data传原始内容 自动编码json
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
[key: string]: string
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
const option = new URL(url);
const protocol = url.startsWith('https://') ? https : http;
const options = {
hostname: option.hostname,
port: option.port,
path: option.pathname + option.search,
method,
headers,
};
// headers: {
// 'Content-Type': 'application/json',
// 'Content-Length': Buffer.byteLength(postData),
// },
return new Promise((resolve, reject) => {
const req = protocol.request(options, (res: http.IncomingMessage) => {
let responseBody = '';
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
});
}
// 请求和回复都是JSON data传原始内容 自动编码json
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
[key: string]: string
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
const option = new URL(url);
const protocol = url.startsWith('https://') ? https : http;
const options = {
hostname: option.hostname,
port: option.port,
path: option.pathname + option.search,
method: method,
headers: headers,
};
// headers: {
// 'Content-Type': 'application/json',
// 'Content-Length': Buffer.byteLength(postData),
// },
return new Promise((resolve, reject) => {
const req = protocol.request(options, (res: http.IncomingMessage) => {
let responseBody = '';
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
});
res.on('end', () => {
try {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
if (isJsonRet) {
const responseJson = JSON.parse(responseBody);
resolve(responseJson as T);
} else {
resolve(responseBody as T);
}
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
}
} catch (parseError: unknown) {
reject(new Error((parseError as Error).message));
}
});
});
req.on('error', (error: Error) => {
reject(error);
});
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
if (isArgJson) {
req.write(JSON.stringify(data));
} else {
req.write(data);
}
res.on('end', () => {
try {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
if (isJsonRet) {
const responseJson = JSON.parse(responseBody);
resolve(responseJson as T);
} else {
resolve(responseBody as T);
}
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
}
req.end();
} catch (parseError: unknown) {
reject(new Error((parseError as Error).message));
}
});
}
});
// 请求返回都是原始内容
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false);
}
req.on('error', (error: Error) => {
reject(error);
});
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
if (isArgJson) {
req.write(JSON.stringify(data));
} else {
req.write(data);
}
}
req.end();
});
}
// 请求返回都是原始内容
static async HttpGetText (url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false);
}
}

View File

@@ -1,32 +1,32 @@
export type StoreValueType = string | number | boolean | object | null;
export type StoreValue<T extends StoreValueType = StoreValueType> = {
value: T;
expiresAt?: number;
value: T;
expiresAt?: number;
};
class Store {
// 使用Map存储键值对
private store: Map<string, StoreValue>;
// 定时清理器
private cleanerTimer: NodeJS.Timeout;
// 用于分批次扫描的游标
private scanCursor: number = 0;
// 使用Map存储键值对
private store: Map<string, StoreValue>;
// 定时清理器
private cleanerTimer: NodeJS.Timeout;
// 用于分批次扫描的游标
private scanCursor: number = 0;
/**
/**
* Store
* @param cleanInterval 清理间隔
* @param scanLimit 扫描限制(每次最多检查的键数)
*/
constructor(
cleanInterval: number = 1000, // 默认1秒执行一次
private scanLimit: number = 100 // 每次最多检查100个键
) {
this.store = new Map();
this.cleanerTimer = setInterval(() => this.cleanupExpired(), cleanInterval);
}
constructor (
cleanInterval: number = 1000, // 默认1秒执行一次
private scanLimit: number = 100 // 每次最多检查100个键
) {
this.store = new Map();
this.cleanerTimer = setInterval(() => this.cleanupExpired(), cleanInterval);
}
/**
/**
* 设置键值对
* @param key 键
* @param value 值
@@ -34,155 +34,155 @@ class Store {
* @returns void
* @example store.set('key', 'value', 60)
*/
set<T extends StoreValueType>(key: string, value: T, ttl?: number): void {
if (ttl && ttl <= 0) {
this.del(key);
return;
}
const expiresAt = ttl ? Date.now() + ttl * 1000 : undefined;
this.store.set(key, { value, expiresAt });
set<T extends StoreValueType>(key: string, value: T, ttl?: number): void {
if (ttl && ttl <= 0) {
this.del(key);
return;
}
const expiresAt = ttl ? Date.now() + ttl * 1000 : undefined;
this.store.set(key, { value, expiresAt });
}
/**
/**
* 清理过期键
*/
private cleanupExpired(): void {
const now = Date.now();
const keys = Array.from(this.store.keys());
let scanned = 0;
private cleanupExpired (): void {
const now = Date.now();
const keys = Array.from(this.store.keys());
let scanned = 0;
// 分批次扫描
while (scanned < this.scanLimit && this.scanCursor < keys.length) {
const key = keys[this.scanCursor++];
const entry = this.store.get(key!)!;
// 分批次扫描
while (scanned < this.scanLimit && this.scanCursor < keys.length) {
const key = keys[this.scanCursor++];
const entry = this.store.get(key!)!;
if (entry.expiresAt && entry.expiresAt < now) {
this.store.delete(key!);
}
if (entry.expiresAt && entry.expiresAt < now) {
this.store.delete(key!);
}
scanned++;
}
// 重置游标(环形扫描)
if (this.scanCursor >= keys.length) {
this.scanCursor = 0;
}
scanned++;
}
/**
// 重置游标(环形扫描)
if (this.scanCursor >= keys.length) {
this.scanCursor = 0;
}
}
/**
* 获取键值
* @param key 键
* @returns T | null
* @example store.get('key')
*/
get<T extends StoreValueType>(key: string): T | null {
this.checkKeyExpiry(key); // 每次访问都检查
const entry = this.store.get(key);
return entry ? (entry.value as T) : null;
}
get<T extends StoreValueType>(key: string): T | null {
this.checkKeyExpiry(key); // 每次访问都检查
const entry = this.store.get(key);
return entry ? (entry.value as T) : null;
}
/**
/**
* 检查键是否过期
* @param key 键
*/
private checkKeyExpiry(key: string): void {
const entry = this.store.get(key);
if (entry?.expiresAt && entry.expiresAt < Date.now()) {
this.store.delete(key);
}
private checkKeyExpiry (key: string): void {
const entry = this.store.get(key);
if (entry?.expiresAt && entry.expiresAt < Date.now()) {
this.store.delete(key);
}
}
/**
/**
* 检查键是否存在
* @param keys 键
* @returns number
* @example store.exists('key1', 'key2')
*/
exists(...keys: string[]): number {
return keys.filter((key) => {
this.checkKeyExpiry(key);
return this.store.has(key);
}).length;
}
exists (...keys: string[]): number {
return keys.filter((key) => {
this.checkKeyExpiry(key);
return this.store.has(key);
}).length;
}
/**
/**
* 关闭存储器
*/
shutdown(): void {
clearInterval(this.cleanerTimer);
this.store.clear();
}
shutdown (): void {
clearInterval(this.cleanerTimer);
this.store.clear();
}
/**
/**
* 删除键
* @param keys 键
* @returns number
* @example store.del('key1', 'key2')
*/
del(...keys: string[]): number {
return keys.reduce((count, key) => (this.store.delete(key) ? count + 1 : count), 0);
}
del (...keys: string[]): number {
return keys.reduce((count, key) => (this.store.delete(key) ? count + 1 : count), 0);
}
/**
/**
* 设置键的过期时间
* @param key 键
* @param seconds 过期时间(秒)
* @returns boolean
* @example store.expire('key', 60)
*/
expire(key: string, seconds: number): boolean {
const entry = this.store.get(key);
if (!entry) return false;
expire (key: string, seconds: number): boolean {
const entry = this.store.get(key);
if (!entry) return false;
entry.expiresAt = Date.now() + seconds * 1000;
return true;
}
entry.expiresAt = Date.now() + seconds * 1000;
return true;
}
/**
/**
* 获取键的过期时间
* @param key 键
* @returns number | null
* @example store.ttl('key')
*/
ttl(key: string): number | null {
const entry = this.store.get(key);
if (!entry) return null;
ttl (key: string): number | null {
const entry = this.store.get(key);
if (!entry) return null;
if (!entry.expiresAt) return -1;
const remaining = entry.expiresAt - Date.now();
return remaining > 0 ? Math.floor(remaining / 1000) : -2;
}
if (!entry.expiresAt) return -1;
const remaining = entry.expiresAt - Date.now();
return remaining > 0 ? Math.floor(remaining / 1000) : -2;
}
/**
/**
* 键值数字递增
* @param key 键
* @returns number
* @example store.incr('key')
*/
incr(key: string): number {
const current = this.get<StoreValueType>(key);
incr (key: string): number {
const current = this.get<StoreValueType>(key);
if (current === null) {
this.set(key, 1, 60);
return 1;
}
let numericValue: number;
if (typeof current === 'number') {
numericValue = current;
} else if (typeof current === 'string') {
if (!/^-?\d+$/.test(current)) {
throw new Error('ERR value is not an integer');
}
numericValue = parseInt(current, 10);
} else {
throw new Error('ERR value is not an integer');
}
const newValue = numericValue + 1;
this.set(key, newValue, 60);
return newValue;
if (current === null) {
this.set(key, 1, 60);
return 1;
}
let numericValue: number;
if (typeof current === 'number') {
numericValue = current;
} else if (typeof current === 'string') {
if (!/^-?\d+$/.test(current)) {
throw new Error('ERR value is not an integer');
}
numericValue = parseInt(current, 10);
} else {
throw new Error('ERR value is not an integer');
}
const newValue = numericValue + 1;
this.set(key, newValue, 60);
return newValue;
}
}
const store = new Store();

View File

@@ -5,12 +5,11 @@ import path from 'node:path';
let osName: string;
try {
osName = os.hostname();
osName = os.hostname();
} catch {
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
}
const homeDir = os.homedir();
export const systemPlatform = os.platform();

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,35 +1,34 @@
import { Worker } from 'worker_threads';
export async function runTask<T, R>(workerScript: string, taskData: T): Promise<R> {
let worker = new Worker(workerScript);
try {
return await new Promise<R>((resolve, reject) => {
worker.on('message', (result: R) => {
if ((result as any)?.log) {
console.error('Worker Log--->:', (result as { log: string }).log);
}
if ((result as any)?.error) {
reject(new Error("Worker error: " + (result as { error: string }).error));
}
resolve(result);
});
export async function runTask<T, R> (workerScript: string, taskData: T): Promise<R> {
const worker = new Worker(workerScript);
try {
return await new Promise<R>((resolve, reject) => {
worker.on('message', (result: R) => {
if ((result as any)?.log) {
console.error('Worker Log--->:', (result as { log: string }).log);
}
if ((result as any)?.error) {
reject(new Error('Worker error: ' + (result as { error: string }).error));
}
resolve(result);
});
worker.on('error', (error) => {
reject(new Error(`Worker error: ${error.message}`));
});
worker.on('error', (error) => {
reject(new Error(`Worker error: ${error.message}`));
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
worker.postMessage(taskData);
});
} catch (error: unknown) {
throw new Error(`Failed to run task: ${(error as Error).message}`);
} finally {
// Ensure the worker is terminated after the promise is settled
worker.terminate();
}
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
worker.postMessage(taskData);
});
} catch (error: unknown) {
throw new Error(`Failed to run task: ${(error as Error).message}`);
} finally {
// Ensure the worker is terminated after the promise is settled
worker.terminate();
}
}