diff --git a/manifest.json b/manifest.json index 9531deae..fc3ea24d 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "NapCatQQ", "slug": "NapCat.Framework", "description": "高性能的 OneBot 11 协议实现", - "version": "3.6.12", + "version": "3.6.16", "icon": "./logo.png", "authors": [ { diff --git a/package.json b/package.json index 7d6dc72b..80e1823a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "napcat", "private": true, "type": "module", - "version": "3.6.12", + "version": "3.6.16", "scripts": { "build:framework": "vite build --mode framework", "build:shell": "vite build --mode shell", @@ -29,7 +29,6 @@ "@typescript-eslint/parser": "^8.3.0", "ajv": "^8.13.0", "async-mutex": "^0.5.0", - "chalk": "^5.3.0", "commander": "^12.1.0", "cors": "^2.8.5", "eslint": "^9.14.0", @@ -44,12 +43,12 @@ "typescript-eslint": "^8.13.0", "vite": "^5.2.6", "vite-plugin-cp": "^4.0.8", - "vite-tsconfig-paths": "^5.1.0" + "vite-tsconfig-paths": "^5.1.0", + "winston": "^3.17.0" }, "dependencies": { "express": "^5.0.0", "fluent-ffmpeg": "^2.1.2", - "log4js": "^6.9.1", "qrcode-terminal": "^0.12.0", "silk-wasm": "^3.6.1", "ws": "^8.18.0" diff --git a/src/common/log.ts b/src/common/log.ts index e836dfe7..50e04173 100644 --- a/src/common/log.ts +++ b/src/common/log.ts @@ -1,7 +1,7 @@ -import log4js, { Configuration } from 'log4js'; +import winston, { format, transports } from 'winston'; import { truncateString } from '@/common/helper'; import path from 'node:path'; -import chalk from 'chalk'; +import fs from 'node:fs'; import { AtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core'; export enum LogLevel { @@ -27,97 +27,132 @@ function getFormattedTimestamp() { 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; + logger: winston.Logger; 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.logger = winston.createLogger({ + level: 'debug', + format: format.combine( + format.timestamp({ format: 'YYYY-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: '', uin: '', uid: '' }); + this.cleanOldLogs(logDir); + } + + cleanOldLogs(logDir: string) { + const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + fs.readdir(logDir, (err, files) => { + if (err) { + this.logger.error('Failed to read log directory', err); + return; + } + files.forEach(file => { + const filePath = path.join(logDir, file); + fs.stat(filePath, (err, stats) => { + if (err) { + this.logger.error('Failed to get file stats', err); + return; + } + if (stats.mtime.getTime() < oneWeekAgo) { + fs.unlink(filePath, err => { + if (err) { + if (err.code === 'ENOENT') { + this.logger.warn(`File already deleted: ${file}`); + } else { + this.logger.error('Failed to delete old log file', err); + } + } else { + this.logger.info(`Deleted old log file: ${file}`); + } + }); + } + }); + }); + }); } setFileAndConsoleLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) { - this.logConfig.categories.file.level = fileLogLevel; - this.logConfig.categories.console.level = consoleLogLevel; - log4js.configure(this.logConfig); + 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, 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); + 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[]) { - 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; + 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)))); } - logMsg += msgItem + ' '; - } - return logMsg; + return msgItem; + }).join(' '); } - _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, '')); + 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) { + this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, '')); } } log(...args: any[]) { - // info 等级 this._log(LogLevel.INFO, ...args); } @@ -140,12 +175,11 @@ export class LogWrapper { logMessage(msg: RawMessage, selfInfo: SelfInfo) { const isSelfSent = msg.senderUin === selfInfo.uin; - // Intercept grey tip if (msg.elements[0]?.elementType === ElementType.GreyTip) { return; } - this.log(`${isSelfSent ? '发送 ->' : '接收 <-' } ${rawMessageToText(msg)}`); + this.log(`${isSelfSent ? '发送 ->' : '接收 <-'} ${rawMessageToText(msg)}`); } } @@ -167,12 +201,10 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string { } } else if (msg.chatType == ChatType.KCHATTYPEDATALINE) { tokens.push('移动设备'); - } else /* temp */ { + } else { tokens.push(`临时消息 (${msg.peerUin})`); } - // message content - function msgElementToText(element: MessageElement) { if (element.textElement) { if (element.textElement.atType === AtType.notAt) { @@ -190,11 +222,11 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string { record => element.replyElement!.sourceMsgIdInRecords === record.msgId, ); return `[回复消息 ${recordMsgOrNull && - recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'// 非转发消息; 否则定位不到 + recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020' ? rawMessageToText(recordMsgOrNull, recursiveLevel + 1) : `未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})` - }]`; + }]`; } if (element.picElement) { @@ -245,4 +277,4 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string { } return tokens.join(' '); -} +} \ No newline at end of file diff --git a/src/common/version.ts b/src/common/version.ts index 41f7c13d..8c28d84f 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1 +1 @@ -export const napCatVersion = '3.6.12'; +export const napCatVersion = '3.6.16'; diff --git a/src/core/listeners/NodeIKernelMsgListener.ts b/src/core/listeners/NodeIKernelMsgListener.ts index 08636920..26739f4c 100644 --- a/src/core/listeners/NodeIKernelMsgListener.ts +++ b/src/core/listeners/NodeIKernelMsgListener.ts @@ -1,4 +1,4 @@ -import { ChatType, RawMessage } from '@/core/entities'; +import { ChatType, KickedOffLineInfo, RawMessage } from '@/core/entities'; import { CommonFileInfo } from '@/core'; export interface OnRichMediaDownloadCompleteParams { @@ -212,7 +212,7 @@ export class NodeIKernelMsgListener { } - onKickedOffLine(kickedInfo: unknown) { + onKickedOffLine(kickedInfo: KickedOffLineInfo) { } diff --git a/src/framework/napcat.ts b/src/framework/napcat.ts index 60607d0f..9c947594 100644 --- a/src/framework/napcat.ts +++ b/src/framework/napcat.ts @@ -23,6 +23,14 @@ export async function NCoreInitFramework( ) { //在进入本层前是否登录未进行判断 console.log('NapCat Framework App Loading...'); + + process.on('uncaughtException', (err) => { + console.log('[NapCat] [Error] Unhandled Exception:', err.message); + }); + process.on('unhandledRejection', (reason, promise) => { + console.log('[NapCat] [Error] unhandledRejection:', reason); + }); + const pathWrapper = new NapCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index f83aec82..83892810 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -185,6 +185,8 @@ export class OneBotMsgApi { file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + _.key + ".jpg"), path: url, url: url, + key: _.key, + emoji_id: _.emojiId, file_unique: _.key }, }; diff --git a/src/onebot/event/notice/BotOfflineEvent.ts b/src/onebot/event/notice/BotOfflineEvent.ts new file mode 100644 index 00000000..6718a561 --- /dev/null +++ b/src/onebot/event/notice/BotOfflineEvent.ts @@ -0,0 +1,16 @@ +import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; +import { NapCatCore } from '@/core'; + +export class BotOfflineEvent extends OB11BaseNoticeEvent { + notice_type = 'bot_offline'; + user_id: number; + tag: string = 'BotOfflineEvent'; + message: string = 'BotOfflineEvent'; + + public constructor(core: NapCatCore, tag: string, message: string) { + super(core); + this.user_id = +core.selfInfo.uin; + this.tag = tag; + this.message = message; + } +} diff --git a/src/onebot/index.ts b/src/onebot/index.ts index d84d299c..7f304a59 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -46,6 +46,8 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal import { LRUCache } from '@/common/lru-cache'; import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener'; import { Native } from '@/native'; +//import { decodeMessage, decodeRecallGroup } from '@/core/packet/proto/old/Message'; +import { BotOfflineEvent } from './event/notice/BotOfflineEvent'; //OneBot实现类 export class NapCatOneBot11Adapter { @@ -343,7 +345,11 @@ export class NapCatOneBot11Adapter { } } }; - + msgListener.onKickedOffLine = async (kick) => { + let event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc); + this.networkManager.emitEvent(event) + .catch(e => this.context.logger.logError.bind(this.context.logger)('处理Bot掉线失败', e)); + } this.context.session.getMsgService().addKernelMsgListener( proxiedListenerOf(msgListener, this.context.logger), ); diff --git a/src/shell/napcat.ts b/src/shell/napcat.ts index cc8890de..f0d80eac 100644 --- a/src/shell/napcat.ts +++ b/src/shell/napcat.ts @@ -36,7 +36,12 @@ const cmdOptions = program.opts(); // NapCat Shell App ES 入口文件 export async function NCoreInitShell() { console.log('NapCat Shell App Loading...'); - + process.on('uncaughtException', (err) => { + console.log('[NapCat] [Error] Unhandled Exception:', err.message); + }); + process.on('unhandledRejection', (reason, promise) => { + console.log('[NapCat] [Error] unhandledRejection:', reason); + }); const pathWrapper = new NapCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); @@ -227,7 +232,7 @@ export async function NCoreInitShell() { logger.log(`可用于快速登录的 QQ:\n${historyLoginList .map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`) .join('\n') - }`); + }`); } loginService.getQRCodePicture(); } diff --git a/static/assets/renderer.js b/static/assets/renderer.js index 823be729..a6a2574c 100644 --- a/static/assets/renderer.js +++ b/static/assets/renderer.js @@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) { SettingItem( 'Napcat', void 0, - SettingButton("V3.6.12", "napcat-update-button", "secondary") + SettingButton("V3.6.16", "napcat-update-button", "secondary") ) ]), SettingList([ diff --git a/vite.config.ts b/vite.config.ts index b74c214c..aa4c0df8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,7 +4,7 @@ import { resolve } from 'path'; import nodeResolve from '@rollup/plugin-node-resolve'; import { builtinModules } from 'module'; //依赖排除 -const external = ['silk-wasm', 'ws', 'express', 'fluent-ffmpeg', 'log4js', 'qrcode-terminal']; +const external = ['silk-wasm', 'ws', 'express', 'fluent-ffmpeg', 'qrcode-terminal']; const nodeModules = [...builtinModules, builtinModules.map(m => `node:${m}`)].flat(); function genCpModule(module: string) { return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false };