HumanBreak/packages/common/src/logger.ts
2025-06-12 15:23:00 +08:00

216 lines
5.7 KiB
TypeScript

import { debounce } from 'lodash-es';
import logInfo from './logger.json';
export const enum LogLevel {
/** 输出所有,包括日志 */
LOG,
/** 报错、严重警告和警告 */
WARNING,
/** 仅报错 */
ERROR
}
interface LoggerCatchInfo {
code?: number;
level: LogLevel;
message: string;
}
interface LoggerCatchReturns<T> {
ret: T;
info: LoggerCatchInfo[];
}
let logTip: HTMLSpanElement;
if (!main.replayChecking) {
const tip = document.createElement('span');
logTip = tip;
tip.style.position = 'fixed';
tip.style.right = '0';
tip.style.bottom = '0';
tip.style.height = '20px';
tip.style.width = 'auto';
tip.style.textAlign = 'right';
tip.style.padding = '0 5px';
tip.style.fontSize = '16px';
tip.style.fontFamily = 'Arial';
tip.style.display = 'none';
tip.style.margin = '2px';
document.body.appendChild(tip);
}
const hideTipText = debounce(() => {
if (main.replayChecking) return;
logTip.style.display = 'none';
}, 5000);
const nums = new Set('1234567890');
export interface ILoggerInfo {
error: Record<number, string>;
warn: Record<number, string>;
}
export class Logger {
enabled: boolean = true;
private catching: boolean = false;
private catchedInfo: LoggerCatchInfo[] = [];
private catchStack: LoggerCatchInfo[][] = [];
constructor(
public readonly level: LogLevel,
public readonly info: ILoggerInfo
) {}
private parseInfo(text: string, ...params: string[]) {
let pointer = -1;
let str = '';
let inParam = false;
let paramNum = '';
while (++pointer < text.length) {
const char = text[pointer];
if (char === '$' && text[pointer - 1] !== '\\') {
inParam = true;
continue;
}
if (inParam) {
if (nums.has(char)) {
paramNum += char;
}
if (!nums.has(text[pointer + 1])) {
inParam = false;
const num = Number(paramNum);
paramNum = '';
str += params[num - 1] ?? '[not delivered]';
}
continue;
}
str += char;
}
return str;
}
/**
* 输出报错信息
* @param code 错误代码,每个错误都应当使用唯一的错误代码
* @param params 参数
*/
error(code: number, ...params: string[]) {
const info = this.info.error[code];
if (!info) {
logger.error(16, 'error', code.toString());
return;
}
const text = this.parseInfo(info, ...params);
if (this.catching) {
this.catchedInfo.push({
level: LogLevel.ERROR,
message: text,
code
});
}
if (this.level <= LogLevel.ERROR && this.enabled) {
if (!main.replayChecking) {
logTip.style.color = 'lightcoral';
logTip.style.display = 'block';
logTip.textContent = `Error thrown, please check in console.`;
hideTipText();
}
const n = Math.floor(code / 50) + 1;
const n2 = code % 50;
const url = `${location.origin}/_docs/logger/error/error${n}.html#error-code-${n2}`;
console.error(`[ERROR Code ${code}] ${text} See ${url}`);
}
}
/**
* 输出警告信息
* @param code 警告代码
* @param text 警告信息
*/
warn(code: number, ...params: string[]) {
const info = this.info.warn[code];
if (!info) {
logger.error(16, 'warn', code.toString());
return;
}
const text = this.parseInfo(info, ...params);
if (this.catching) {
this.catchedInfo.push({
level: LogLevel.ERROR,
message: text,
code
});
}
if (this.level <= LogLevel.WARNING && this.enabled) {
if (!main.replayChecking) {
logTip.style.color = 'gold';
logTip.style.display = 'block';
logTip.textContent = `Warning thrown, please check in console.`;
hideTipText();
}
const n = Math.floor(code / 50) + 1;
const n2 = code % 50;
const url = `${location.origin}/_docs/logger/warn/warn${n}.html#warn-code-${n2}`;
console.warn(`[WARNING Code ${code}] ${text}. See ${url}`);
}
}
/**
* 输出日志
* @param text 日志信息
*/
log(text: string) {
if (this.catching) {
this.catchedInfo.push({
level: LogLevel.ERROR,
message: text
});
}
if (this.level <= LogLevel.LOG && this.enabled) {
console.log(`[LOG] ${text}`);
}
}
catch<T>(fn: () => T): LoggerCatchReturns<T> {
const before = this.enabled;
this.catchedInfo = [];
this.catchStack.push(this.catchedInfo);
this.disable();
this.catching = true;
const ret = fn();
if (this.catchStack.length === 0) {
this.catching = false;
} else {
this.catching = true;
}
if (before) this.enable();
this.catchStack.pop();
const last = this.catchStack?.at(-1);
const info = this.catchedInfo;
this.catchedInfo = last ?? [];
return { ret, info };
}
disable() {
this.enabled = false;
}
enable() {
this.enabled = true;
}
}
export const logger = new Logger(LogLevel.LOG, logInfo);