diff --git a/src/common/log.ts b/src/common/log.ts index 60603b58..7b34f46a 100644 --- a/src/common/log.ts +++ b/src/common/log.ts @@ -3,7 +3,7 @@ import { truncateString } from '@/common/helper'; import path from 'node:path'; import fs from 'node:fs'; import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core'; - +import EventEmitter from 'node:events'; export enum LogLevel { DEBUG = 'debug', INFO = 'info', @@ -24,6 +24,36 @@ function getFormattedTimestamp() { 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[] = []; + + subscribe(listener: LogListener) { + for (const history of Subscription.history) { + try { + listener(history); + } catch (_) { + // ignore + } + } + 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; @@ -47,7 +77,7 @@ export class LogWrapper { filename: logPath, level: 'debug', maxsize: 5 * 1024 * 1024, // 5MB - maxFiles: 5 + maxFiles: 5, }), new transports.Console({ format: format.combine( @@ -56,9 +86,9 @@ export class LogWrapper { const userInfo = meta.userInfo ? `${meta.userInfo} | ` : ''; return `${timestamp} [${level}] ${userInfo}${message}`; }) - ) - }) - ] + ), + }), + ], }); this.setLogSelfInfo({ nick: '', uid: '' }); @@ -72,7 +102,7 @@ export class LogWrapper { this.logger.error('Failed to read log directory', err); return; } - files.forEach(file => { + files.forEach((file) => { const filePath = path.join(logDir, file); this.deleteOldLogFile(filePath, oneWeekAgo); }); @@ -86,7 +116,7 @@ export class LogWrapper { return; } if (stats.mtime.getTime() < oneWeekAgo) { - fs.unlink(filePath, err => { + fs.unlink(filePath, (err) => { if (err) { if (err.code === 'ENOENT') { this.logger.warn(`File already deleted: ${filePath}`); @@ -111,7 +141,7 @@ export class LogWrapper { }); } - setLogSelfInfo(selfInfo: { nick: string, uid: string }) { + setLogSelfInfo(selfInfo: { nick: string; uid: string }) { const userInfo = `${selfInfo.nick}`; this.logger.defaultMeta = { userInfo }; } @@ -135,14 +165,16 @@ export class LogWrapper { } 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; - }).join(' '); + 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; + }) + .join(' '); } _log(level: LogLevel, ...args: any[]) { @@ -155,6 +187,7 @@ export class LogWrapper { // eslint-disable-next-line no-control-regex this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, '')); } + logSubscription.notify(message); } log(...args: any[]) { @@ -282,13 +315,10 @@ function textElementToText(textElement: any): string { } 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})` + 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})` }]`; -} \ No newline at end of file +} diff --git a/src/webui/src/api/Log.ts b/src/webui/src/api/Log.ts index 5b9b8c2b..1684183d 100644 --- a/src/webui/src/api/Log.ts +++ b/src/webui/src/api/Log.ts @@ -1,6 +1,7 @@ import type { RequestHandler } from 'express'; import { sendError, sendSuccess } from '../utils/response'; import { WebUiConfigWrapper } from '../helper/config'; +import { logSubscription } from '@/common/log'; // 日志记录 export const LogHandler: RequestHandler = async (req, res) => { @@ -17,3 +18,17 @@ export const LogListHandler: RequestHandler = async (_, res) => { const logList = WebUiConfigWrapper.GetLogsList(); return sendSuccess(res, logList); }; +// 实时日志(SSE) +export const LogRealTimeHandler: RequestHandler = async (req, res) => { + const listener = (log: string) => { + try { + res.write(log + '\n'); + } catch (error) { + // ignore + } + }; + logSubscription.subscribe(listener); + req.on('close', () => { + logSubscription.unsubscribe(listener); + }); +}; diff --git a/src/webui/src/router/Log.ts b/src/webui/src/router/Log.ts index 46187eeb..b72a2245 100644 --- a/src/webui/src/router/Log.ts +++ b/src/webui/src/router/Log.ts @@ -1,9 +1,13 @@ import { Router } from 'express'; -import { LogHandler, LogListHandler } from '../api/Log'; +import { LogHandler, LogListHandler, LogRealTimeHandler } from '../api/Log'; + const router = Router(); // router:读取日志内容 router.get('/GetLog', LogHandler); // router:读取日志列表 router.get('/GetLogList', LogListHandler); +// router:实时日志 +router.get('/GetLogRealTime', LogRealTimeHandler); + export { router as LogRouter };