Files
ele-HeiXiu/electron/main/logger.ts
YoungestSongMo 337d6c1205 feat: 构建系统重构 + VCHSM 架构迁移 + 右键菜单修复 + 尺寸参数统一
## 构建、打包与分发系统重构 (build/)

- 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本

- electron-builder 配置从 package.json 提取为 build/electron-builder.js

- 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数

- OSS 路径按渠道隔离

- 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验

- 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成

- updater.ts 发送 channel/edition/deviceFingerprint

- 新增 src/shared/utils/rollout.ts 灰度测试工具

- 新增 10 条 NPM 脚本

## VCHSM 五层架构迁移

- 7 个业务模块 + ~500 处导入路径同步

## 修复

- MediaCardContextMenu 改用共享 ContextMenu 组件

- ComboBox 尺寸参数显示统一 (size-format.ts)

## 文档

- UpdateA.md / build/README.md / README.md / .env.example 更新

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 18:41:07 +08:00

258 lines
6.9 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 = '';
let logDirResolver: (() => string) | null = null;
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 = logDirResolver ? logDirResolver() : 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);
},
};
/** 注入自定义日志目录解析器(在 initLogger() 之前调用) */
export function setLogDirResolver(resolver: () => string): void {
logDirResolver = resolver;
}
/** 预热日志模块(在 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;
}
}