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 字段 / 数据库字段说明
This commit is contained in:
2026-06-04 11:03:25 +08:00
parent 337234443a
commit ae733016bb
21 changed files with 1245 additions and 154 deletions

251
electron/main/logger.ts Normal file
View File

@@ -0,0 +1,251 @@
// ============================================================
// 主进程日志核心 — 文件写入 / 轮转 / 清理
//
// 架构:
// - 日志写入 {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;
}
}