video-flow-toon/src/logger.ts
2026-02-10 17:41:18 +08:00

157 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as fs from "fs";
import * as path from "path";
type LogLevel = "log" | "info" | "warn" | "error" | "debug";
type ConsoleMethod = (...args: unknown[]) => void;
function getLogDir(): string {
const isElectron = typeof process.versions?.electron !== "undefined";
if (isElectron) {
const { app } = require("electron");
return path.join(app.getPath("userData"), "logs");
}
return path.join(process.cwd(), "logs");
}
const LOG_DIR = getLogDir();
const LOG_FILE = path.join(LOG_DIR, "app.log");
const MAX_SIZE = 1000 * 1024 * 1024;
const LEVELS: LogLevel[] = ["log", "info", "warn", "error", "debug"];
class Logger {
private stream: fs.WriteStream | null = null;
private originalConsole: Partial<Record<LogLevel, ConsoleMethod>> = {};
private originalStdoutWrite: typeof process.stdout.write | null = null;
private originalStderrWrite: typeof process.stderr.write | null = null;
private isHijacked = false;
init(): this {
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
this.stream = fs.createWriteStream(LOG_FILE, { flags: "a" });
this.hijack();
return this;
}
private formatTime(): string {
const d = new Date();
const p = (n: number, l = 2) => String(n).padStart(l, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(
d.getMilliseconds(),
3,
)}`;
}
private stringify(arg: unknown): string {
if (arg == null) return String(arg);
if (arg instanceof Error) return `${arg.message}\n${arg.stack || ""}`;
if (typeof arg === "object") {
try {
return JSON.stringify(arg);
} catch {
return String(arg);
}
}
return String(arg);
}
private writing = false;
private write(level: LogLevel, args: unknown[]): void {
const line = `[${this.formatTime()}] [${level.toUpperCase()}] ${args.map((a) => this.stringify(a)).join(" ")}\n`;
if (this.stream && !this.stream.destroyed) this.stream.write(line);
this.checkRotate();
}
private writeRaw(chunk: any): void {
if (this.writing) return;
this.writing = true;
try {
let str = typeof chunk === "string" ? chunk : chunk?.toString?.("utf-8") ?? "";
str = str.replace(/\x1B\[\d*m/g, ""); // 去除 ANSI 颜色码
if (str.trim() && this.stream && !this.stream.destroyed) this.stream.write(str.endsWith("\n") ? str : str + "\n");
} finally {
this.writing = false;
}
}
private checkRotate(): void {
try {
if (!fs.existsSync(LOG_FILE) || fs.statSync(LOG_FILE).size < MAX_SIZE) return;
this.stream?.end();
// 单文件轮转:保留后半部分日志
const content = fs.readFileSync(LOG_FILE, "utf-8");
const half = content.slice(content.length >>> 1);
const firstNewline = half.indexOf("\n");
fs.writeFileSync(LOG_FILE, firstNewline >= 0 ? half.slice(firstNewline + 1) : half);
this.stream = fs.createWriteStream(LOG_FILE, { flags: "a" });
} catch {}
}
private hijack(): void {
if (this.isHijacked) return;
// 劫持 console 方法
for (const level of LEVELS) {
const original = console[level];
if (typeof original !== "function") continue;
this.originalConsole[level] = original.bind(console);
(console as any)[level] = (...args: unknown[]) => {
this.writing = true;
this.write(level, args);
this.originalConsole[level]!(...args);
this.writing = false;
};
}
// 劫持 stdout/stderr捕获 morgan 等直接写 stdout 的输出)
this.originalStdoutWrite = process.stdout.write.bind(process.stdout);
this.originalStderrWrite = process.stderr.write.bind(process.stderr);
process.stdout.write = ((chunk: any, ...rest: any[]) => {
this.writeRaw(chunk);
return this.originalStdoutWrite!(chunk, ...rest);
}) as typeof process.stdout.write;
process.stderr.write = ((chunk: any, ...rest: any[]) => {
this.writeRaw(chunk);
return this.originalStderrWrite!(chunk, ...rest);
}) as typeof process.stderr.write;
this.isHijacked = true;
}
/** 导出日志内容 */
exportLogs(): string {
if (!fs.existsSync(LOG_FILE)) return "";
return fs.readFileSync(LOG_FILE, "utf-8");
}
/** 清空日志 */
clear(): void {
this.stream?.end();
if (fs.existsSync(LOG_FILE)) fs.unlinkSync(LOG_FILE);
this.stream = fs.createWriteStream(LOG_FILE, { flags: "a" });
}
/** 关闭日志 */
close(): void {
if (this.isHijacked) {
for (const level of LEVELS) {
const original = this.originalConsole[level];
if (original) (console as any)[level] = original;
}
this.originalConsole = {};
if (this.originalStdoutWrite) process.stdout.write = this.originalStdoutWrite;
if (this.originalStderrWrite) process.stderr.write = this.originalStderrWrite;
this.originalStdoutWrite = null;
this.originalStderrWrite = null;
this.isHijacked = false;
}
this.stream?.end();
this.stream = null;
}
}
const logger = new Logger().init();
export default logger;