import log4js, { Configuration } from 'log4js'; import { truncateString } from '@/common/helper'; import path from 'node:path'; import chalk from 'chalk'; import { AtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core'; export enum LogLevel { 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}`; } export class LogWrapper { fileLogEnabled = true; consoleLogEnabled = true; logConfig: Configuration; loggerConsole: log4js.Logger; loggerFile: log4js.Logger; loggerDefault: log4js.Logger; // eslint-disable-next-line no-control-regex colorEscape = /\x1B[@-_][0-?]*[ -/]*[@-~]/g; constructor(logDir: string) { const filename = `${getFormattedTimestamp()}.log`; const logPath = path.join(logDir, filename); this.logConfig = { appenders: { FileAppender: { // 输出到文件的appender type: 'file', filename: logPath, // 指定日志文件的位置和文件名 maxLogSize: 10485760, // 日志文件的最大大小(单位:字节),这里设置为10MB layout: { type: 'pattern', pattern: '%d{yyyy-MM-dd hh:mm:ss} [%p] %X{userInfo} | %m', }, }, ConsoleAppender: { // 输出到控制台的appender type: 'console', layout: { type: 'pattern', pattern: `%d{yyyy-MM-dd hh:mm:ss} [%[%p%]] ${chalk.magenta('%X{userInfo}')} | %m`, }, }, }, categories: { default: { appenders: ['FileAppender', 'ConsoleAppender'], level: 'debug' }, // 默认情况下同时输出到文件和控制台 file: { appenders: ['FileAppender'], level: 'debug' }, console: { appenders: ['ConsoleAppender'], level: 'debug' }, }, }; log4js.configure(this.logConfig); this.loggerConsole = log4js.getLogger('console'); this.loggerFile = log4js.getLogger('file'); this.loggerDefault = log4js.getLogger('default'); this.setLogSelfInfo({ nick: '', uin: '', uid: '' }); } setFileAndConsoleLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) { this.logConfig.categories.file.level = fileLogLevel; this.logConfig.categories.console.level = consoleLogLevel; log4js.configure(this.logConfig); } setLogSelfInfo(selfInfo: { nick: string, uin: string, uid: string }) { const userInfo = `${selfInfo.nick}(${selfInfo.uin})`; this.loggerConsole.addContext('userInfo', userInfo); this.loggerFile.addContext('userInfo', userInfo); this.loggerDefault.addContext('userInfo', userInfo); } setFileLogEnabled(isEnabled: boolean) { this.fileLogEnabled = isEnabled; } setConsoleLogEnabled(isEnabled: boolean) { this.consoleLogEnabled = isEnabled; } formatMsg(msg: any[]) { let logMsg = ''; for (const msgItem of msg) { if (msgItem instanceof Error) { // 判断是否是错误 logMsg += msgItem.stack + ' '; continue; } else if (typeof msgItem === 'object') { // 判断是否是对象 const obj = JSON.parse(JSON.stringify(msgItem, null, 2)); logMsg += JSON.stringify(truncateString(obj)) + ' '; continue; } logMsg += msgItem + ' '; } return logMsg; } _log(level: LogLevel, ...args: any[]) { if (this.consoleLogEnabled) { this.loggerConsole[level](this.formatMsg(args)); } if (this.fileLogEnabled) { this.loggerFile[level](this.formatMsg(args).replace(this.colorEscape, '')); } } log(...args: any[]) { // info 等级 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; this.log(`${isSelfSent ? '发送 ->' : '接收 <-' } ${rawMessageToText(msg)}`); } } 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) { tokens.push(`群聊 (群 ${msg.peerUin} 的 ${msg.senderUin})`); } else if (msg.chatType == ChatType.KCHATTYPEDATALINE) { tokens.push('移动设备'); } else /* temp */ { tokens.push(`临时消息 (${msg.peerUin})`); } // message content function msgElementToText(element: MessageElement) { if (element.textElement) { if (element.textElement.atType === AtType.notAt) { const originalContentLines = element.textElement.content.split('\n'); return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`; } else if (element.textElement.atType === AtType.atAll) { return `@全体成员`; } else if (element.textElement.atType === AtType.atUser) { return `${element.textElement.content} (${element.textElement.atUid})`; } } if (element.replyElement) { const recordMsgOrNull = msg.records.find( record => element.replyElement!.sourceMsgIdInRecords === record.msgId, ); return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'// 非转发消息; 否则定位不到 ? rawMessageToText(recordMsgOrNull, recursiveLevel + 1) : `未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})` }]`; } if (element.picElement) { return '[图片]'; } if (element.fileElement) { return `[文件 ${element.fileElement.fileName}]`; } if (element.videoElement) { return '[视频]'; } if (element.pttElement) { return `[语音 ${element.pttElement.duration}s]`; } if (element.arkElement) { return '[卡片消息]'; } if (element.faceElement) { return `[表情 ${element.faceElement.faceText ?? ''}]`; } if (element.marketFaceElement) { return element.marketFaceElement.faceName; } if (element.markdownElement) { return '[Markdown 消息]'; } if (element.multiForwardMsgElement) { return '[转发消息]'; } if (element.elementType === ElementType.GreyTip) { return '[灰条消息]'; } return `[未实现 (ElementType = ${element.elementType})]`; } for (const element of msg.elements) { tokens.push(msgElementToText(element)); } return tokens.join(' '); }