Files
ele-HeiXiu/electron/main/logger.ts
YoungestSongMo ae733016bb feat: 日志模块 + netRequest 封装 + PROJECT.md 更新
日志模块(文件持久化 + 事件总线联动 + 脱敏)
    新增:
      - shared/types/logging.ts         — LogLevel / LogCategory / LogEntry 类型
      - electron/main/logger.ts         — 主进程日志核心(JSON Lines / 每日轮转 / 7天清理)
      - electron/main/log-ipc.ts        — 渲染进程日志 IPC 通道接收
      - src/utils/logger.ts             — 渲染进程日志器(IPC 转发 / DEV 控制台 / 事件总线钩子)
      - shared/utils/sanitize.ts        — 脱敏工具(ID哈希 / 邮箱 / 手机 / 名称 / Token)
    修改:
      - electron/main.ts                — initLogger + flushLogger + uncaughtException
      - src/main.tsx                    — renderer 全局错误捕获
      - src/utils/event-bus.ts          — setEventLogListener 钩子,emit 自动审计
      - src/services/request.ts         — 拦截器结构化错误分级记录
      - electron/main/updater.ts        — 14处 console → logger 迁移
      - src/components/AppProvider.tsx   — 登录/登出/自动登录 auth 事件日志
      - shared/constants/ipc-channels.ts — 新增 LOG_MESSAGE 通道

  netRequest 封装(Electron net.request 的 axios 风格 API)
    新增:
      - electron/main/net-request.ts     — netRequest.get/post/put/del 命名空间
    修改:
      - electron/main/updater.ts         — fetchUpdateInfo 改用 netRequest(22→12行)

  文档更新
    - PROJECT.md                       — 目录结构 / 状态管理 / 日志系统 / IPC 章节同步现状
    - TODO.md                          — 增量更新改造 + 日志模块待办
    - UpdateA.md                       — API blockmap 字段 / 数据库字段说明
2026-06-04 11:03:25 +08:00

252 lines
6.7 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.
// ============================================================
// 主进程日志核心 — 文件写入 / 轮转 / 清理
//
// 架构:
// - 日志写入 {userData}/logs/app-YYYY-MM-DD.logJSON Lines 格式)
// - 每日轮转:日期变化时自动创建新文件
// - 保留策略7 天前旧文件自动删除
// - 写入队列批量冲刷setImmediate 聚合同一 tick 的日志)
// - 惰性初始化app.getPath('userData') 在 ready 后才可用,
// initLogger() 在 whenReady 中显式调用预热
//
// 用法(主进程):
// import { logger, initLogger } from './main/logger';
// logger.info('updater', '检查更新', { version: '0.1.0' });
//
// 渲染进程日志通过 log-ipc.ts IPC 通道写入,调用 writeRendererLog()
// ============================================================
import { app } from 'electron';
import { createWriteStream, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import type { WriteStream } from 'node:fs';
import type { LogEntry, LogLevel, LogCategory, LogErrorSnapshot } from '../../shared/types/logging';
import { sanitizeLogEntry } from '../../shared/utils/sanitize';
// ---------- 内部状态 ----------
let writeStream: WriteStream | null = null;
let currentDate = '';
let logDir = '';
const writeQueue: string[] = [];
let isWriting = false;
const MAX_DAYS = 7;
// ---------- 辅助函数 ----------
function getDateStr(): string {
return new Date().toISOString().slice(0, 10);
}
function getLogFilePath(date: string): string {
return join(logDir, `app-${date}.log`);
}
/** 确保 logs/ 目录存在 */
function ensureDir(): void {
if (logDir) return;
try {
logDir = join(app.getPath('userData'), 'logs');
} catch {
// app.getPath 尚不可用app.ready 之前),静默跳过
return;
}
if (!logDir) return;
try {
mkdirSync(logDir, { recursive: true });
} catch {
/* 目录创建失败 — 后续写入静默丢弃 */
}
}
/** 日期轮转:日切时关闭旧流 → 创建新文件 → 清理过期文件 */
function rotateIfNeeded(): void {
const today = getDateStr();
if (currentDate === today && writeStream) return;
if (writeStream) {
try {
writeStream.end();
} catch {
/* ignore */
}
writeStream = null;
}
try {
writeStream = createWriteStream(getLogFilePath(today), { flags: 'a' });
currentDate = today;
} catch {
writeStream = null;
}
purgeOldLogs();
}
/** 清理超过 MAX_DAYS 天的旧日志文件 */
function purgeOldLogs(): void {
if (!logDir) return;
const now = Date.now();
const maxAge = MAX_DAYS * 24 * 60 * 60 * 1000;
try {
const files = readdirSync(logDir);
for (const file of files) {
if (!file.startsWith('app-') || !file.endsWith('.log')) continue;
try {
const stat = statSync(join(logDir, file));
if (now - stat.mtimeMs > maxAge) {
unlinkSync(join(logDir, file));
}
} catch {
/* skip inaccessible files */
}
}
} catch {
/* 目录读取失败 — 下次轮转再试 */
}
}
/** 批量冲刷写入队列 */
function processQueue(): void {
if (isWriting || writeQueue.length === 0) return;
isWriting = true;
ensureDir();
if (logDir) {
rotateIfNeeded();
if (writeStream) {
const batch = writeQueue.splice(0).join('');
try {
writeStream.write(batch);
} catch {
/* 写入失败 — 数据丢失,但日志不应阻断业务 */
}
} else {
writeQueue.length = 0;
}
} else {
// app ready 前尚无 userData 路径 — 丢弃(此时距 ready 极近)
writeQueue.length = 0;
}
isWriting = false;
}
/** 错误对象 → 可序列化快照 */
function errorToSnapshot(err: Error): LogErrorSnapshot {
return {
name: err.name,
message: err.message,
stack: err.stack,
cause: (err as { cause?: unknown }).cause
? String((err as { cause?: unknown }).cause)
: undefined,
};
}
/** 构建日志条目 */
function createEntry(
level: LogLevel,
category: LogCategory,
message: string,
processType: 'main' | 'renderer',
error?: Error,
context?: Record<string, unknown>,
): LogEntry {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
category,
message,
processType,
};
if (context && Object.keys(context).length > 0) {
entry.context = context;
}
if (error) {
entry.error = errorToSnapshot(error);
}
return entry;
}
/** 内部写入入口 */
function write(
level: LogLevel,
category: LogCategory,
message: string,
error?: Error,
context?: Record<string, unknown>,
): void {
const entry = sanitizeLogEntry(
createEntry(level, category, message, 'main', error, context),
);
writeQueue.push(JSON.stringify(entry) + '\n');
// 首个入队项触发 setImmediate 批量冲刷
if (writeQueue.length === 1) {
setImmediate(processQueue);
}
}
// ---------- 对外 API ----------
/** 主进程日志器(与渲染进程 API 完全一致) */
export const logger = {
debug(category: LogCategory, message: string, context?: Record<string, unknown>): void {
write('DEBUG', category, message, undefined, context);
},
info(category: LogCategory, message: string, context?: Record<string, unknown>): void {
write('INFO', category, message, undefined, context);
},
warn(
category: LogCategory,
message: string,
error?: Error,
context?: Record<string, unknown>,
): void {
write('WARN', category, message, error, context);
},
error(
category: LogCategory,
message: string,
error?: Error,
context?: Record<string, unknown>,
): void {
write('ERROR', category, message, error, context);
},
};
/** 预热日志模块(在 app.whenReady() 中调用) */
export function initLogger(): void {
ensureDir();
if (logDir) {
rotateIfNeeded();
}
}
/**
* 接收渲染进程日志并写入文件
* 由 log-ipc.ts 的 IPC 监听器调用
*/
export function writeRendererLog(entry: LogEntry): void {
writeQueue.push(JSON.stringify(entry) + '\n');
if (writeQueue.length === 1) {
setImmediate(processQueue);
}
}
/** 冲刷所有未写入日志并关闭写入流(在 app.before-quit 中调用) */
export function flushLogger(): void {
if (writeQueue.length > 0) {
processQueue();
}
if (writeStream) {
try {
writeStream.end();
} catch {
/* ignore */
}
writeStream = null;
}
}