Files
dream_pep dfda648147 refactor: 迁移组件库至shadcn/ui并新增版本更新功能
迁移项目内所有HeroUI表单组件至shadcn/ui,统一组件风格并修复HeroUI Switch点击无响应的问题
新增全局版本更新弹窗功能,包含状态管理store、全局弹窗组件与调试页面调试工具
优化配置广播逻辑,添加250ms防抖避免频繁触发页面重渲染
新增开发环境下日志直接输出到终端的能力,完善日志输出规则
调整调试模式设置项位置,从高级设置页移动至关于设置页
为背景层添加pointer-events: none,避免遮挡页面点击事件
新增点击拦截诊断工具至资源调试页面,用于排查控件无法点击的问题
更新Koring.yml配置模板,新增advanced.preLaunchCmd字段
2026-09-05 00:45:06 +08:00

213 lines
7.2 KiB
TypeScript
Raw Permalink 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.
/**
* 统一日志(主进程)。
*
* 规则:
* - 默认(非 debug):warn/error/info 输出到控制台,debug 不输出;
* - 用户开启「调试模式」(config.advanced.debugMode,设置→游戏→高级):
* debug 也输出控制台,并把全部级别写入 userData/koring.log(超过 5MB 自动轮转为 .old);
* - 开发(未打包)运行且开启调试模式时:日志额外/直接输出到启动该进程的终端(stdout/stderr);
* - 渲染进程经 `log:write`ipcRenderer.send)汇入同一套格式/文件。
*/
import electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
const { ipcMain, app } = electron;
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export interface Logger {
debug: (msg: string, ...args: unknown[]) => void;
info: (msg: string, ...args: unknown[]) => void;
warn: (msg: string, ...args: unknown[]) => void;
error: (msg: string, ...args: unknown[]) => void;
}
const MAX_LOG_BYTES = 5 * 1024 * 1024;
/** 开发(未打包)运行:直接跑在终端里,日志写 stdout/stderr 用户即可实时看到 */
const isDevRun = !app.isPackaged;
let debugModeProvider: () => boolean = () => false;
export function setDebugModeProvider(fn: () => boolean): void {
debugModeProvider = fn;
}
export function isDebugMode(): boolean {
try {
return debugModeProvider();
} catch {
return false;
}
}
let logStream: fs.WriteStream | null = null;
let cachedFilePath: string | null = null;
export function getLogFilePath(): string | null {
if (!isDebugMode()) return null;
if (!cachedFilePath) cachedFilePath = path.join(app.getPath('userData'), 'koring.log');
return cachedFilePath;
}
function openLogStream(): void {
if (logStream) return;
const filePath = getLogFilePath();
if (!filePath) return;
try {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
if (fs.existsSync(filePath) && fs.statSync(filePath).size > MAX_LOG_BYTES) {
try {
fs.renameSync(filePath, `${filePath}.old`);
} catch {
// 轮转失败不阻塞
}
}
} catch {
// 目录不可用等
}
logStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
logStream.on('error', () => {
logStream = null;
});
}
function pad(n: number): string {
return String(n).padStart(2, '0');
}
function timestamp(): string {
const d = new Date();
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
function truncateString(value: string, max: number): string {
if (value.length <= max) return value;
return `${value.slice(0, max)}…(+${value.length - max}字符)`;
}
function summarizeArg(value: unknown): unknown {
if (typeof value === 'string') return truncateString(value, 300);
if (value instanceof Error) return truncateString(value.message, 300);
if (typeof value === 'object' && value !== null) {
try {
const json = JSON.stringify(value, (_key, v) => {
if (typeof v === 'string') return truncateString(v, 160);
if (v instanceof Error) return truncateString(v.message, 160);
if (Array.isArray(v) && v.length > 20) return `[Array(${v.length})]`;
return v;
});
return truncateString(json ?? String(value), 400);
} catch {
return truncateString(String(value), 300);
}
}
return value;
}
function write(scope: string, level: LogLevel, args: unknown[]): void {
const msg = args.map((a) => {
const s = summarizeArg(a);
return typeof s === 'string' ? s : String(s);
}).join(' ');
const line = `[${timestamp()}][${scope}][${level.toUpperCase()}] ${msg}`;
const enabled = isDebugMode();
// debug 仅在调试模式可见;info/warn/error 始终输出
const show = level !== 'debug' || enabled;
if (show) {
if (isDevRun) {
// dev(未打包,pnpm dev / electron .):直接写进程 stdout/stderr
// 让详细日志实时出现在启动它的终端里(不依赖外部控制台捕获)。
const text = `${line}\n`;
if (level === 'error' || level === 'warn') process.stderr.write(text);
else process.stdout.write(text);
} else if (level === 'error') console.error(line);
else if (level === 'warn') console.warn(line);
else if (level === 'info') console.info(line);
else console.debug(line);
}
if (!enabled) return;
openLogStream();
if (logStream) {
logStream.write(`${line}\n`);
}
}
function makeLogger(scope: string): Logger {
const bound = (level: LogLevel) => (msg: string, ...args: unknown[]) => write(scope, level, [msg, ...args]);
return {
debug: bound('debug'),
info: bound('info'),
warn: bound('warn'),
error: bound('error'),
};
}
export function createLogger(scope: string): Logger {
return makeLogger(scope);
}
// ---------------- IPC 日志(全局包装 ipcMain.handle ----------------
function isResultLike(value: unknown): value is { success?: boolean; error?: unknown } {
return typeof value === 'object' && value !== null && 'success' in value;
}
function summarize(payload: unknown[]): unknown[] {
return payload.map(summarizeArg);
}
/**
* 包装所有 ipcMain.handle:每次调用记录 channel、耗时与成败。
* 必须在业务 handler 注册前调用(main.ts 顶层)。
*/
export function installIpcLogging(): void {
const rawHandle = ipcMain.handle.bind(ipcMain);
const log = makeLogger('ipc');
(ipcMain as unknown as { handle: typeof ipcMain.handle }).handle = (
channel: string,
listener: (event: Electron.IpcMainInvokeEvent, ...args: unknown[]) => unknown,
) => {
const wrapped = async (event: Electron.IpcMainInvokeEvent, ...args: unknown[]): Promise<unknown> => {
log.debug(`→ ${channel}`, summarize(args));
const started = Date.now();
try {
const result = await listener(event, ...args);
const ms = Date.now() - started;
const failed = isResultLike(result) && result.success === false;
if (failed) {
log.error(`✗ ${channel} (${ms}ms)`, isResultLike(result) ? (result.error ?? 'unknown error') : 'failed');
} else {
log.debug(`← ${channel} ok (${ms}ms)`, summarize([result]));
}
return result;
} catch (err) {
const ms = Date.now() - started;
log.error(`! ${channel} 异常 (${ms}ms)`, err);
throw err;
}
};
return rawHandle(channel, wrapped as never);
};
}
/** 渲染进程日志桥:ipcRenderer.send('log:write', { level, scope, message }) 汇入统一日志 */
export function registerRendererLogBridge(): void {
ipcMain.on('log:write', (_event, payload: unknown) => {
try {
const p = payload as { level?: string; scope?: string; message?: string } | null;
if (!p || typeof p.message !== 'string') return;
const level = (p.level === 'debug' || p.level === 'info' || p.level === 'warn' || p.level === 'error') ? p.level : 'info';
const scope = typeof p.scope === 'string' && p.scope ? p.scope : 'renderer';
write(`renderer/${scope}`, level, [p.message]);
} catch {
// 日志桥异常不抛给渲染进程
}
});
ipcMain.handle('log:getInfo', () => ({
filePath: isDebugMode() ? getLogFilePath() : null,
debugMode: isDebugMode(),
}));
}