From 950bfe9060656f07abe01b74ace9a473e9b5f74b Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Fri, 28 Mar 2025 01:20:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(logger):=20=E6=B7=BB=E5=8A=A0=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E6=A8=A1=E5=9D=97=E5=B9=B6=E9=9B=86?= =?UTF-8?q?=E6=88=90=E5=88=B0=E4=B8=BB=E8=BF=9B=E7=A8=8B=E5=92=8C=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E8=BF=9B=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 `main-error.ts` 和 `preload-error.ts` 模块,用于捕获和处理应用程序中的错误。错误处理模块支持全局错误捕获、日志记录和错误信息格式化。同时,更新了相关配置文件和导入语句,确保错误处理模块在应用程序中正确加载和使用。 --- packages/logger/common.ts | 21 ++++ packages/logger/main-error.ts | 177 ++++++++++++++++++++++++++++++++ packages/logger/main.ts | 23 +---- packages/logger/preload-error.ts | 213 +++++++++++++++++++++++++++++++++++++++ packages/logger/preload.ts | 28 +++-- packages/setting/main.ts | 2 - src/main/index.ts | 1 + src/preload/index.ts | 1 + src/renderer/src/App.vue | 7 +- src/renderer/src/main.ts | 7 +- src/types/errorHandler.d.ts | 5 + tsconfig.node.json | 2 + tsconfig.web.json | 3 +- 13 files changed, 449 insertions(+), 41 deletions(-) create mode 100644 packages/logger/main-error.ts create mode 100644 packages/logger/preload-error.ts create mode 100644 src/types/errorHandler.d.ts diff --git a/packages/logger/common.ts b/packages/logger/common.ts index 08ab7e1..99802f1 100644 --- a/packages/logger/common.ts +++ b/packages/logger/common.ts @@ -8,3 +8,24 @@ export enum LogLevel { FATAL = 5, OFF = 6, } +// 日志级别名称映射 +export const LogLevelName: Record = { + [LogLevel.TRACE]: "TRACE", + [LogLevel.DEBUG]: "DEBUG", + [LogLevel.INFO]: "INFO", + [LogLevel.WARN]: "WARN", + [LogLevel.ERROR]: "ERROR", + [LogLevel.FATAL]: "FATAL", + [LogLevel.OFF]: "OFF", +} + +// 日志颜色映射(控制台输出用) +export const LogLevelColor: Record = { + [LogLevel.TRACE]: "\x1b[90m", // 灰色 + [LogLevel.DEBUG]: "\x1b[36m", // 青色 + [LogLevel.INFO]: "\x1b[32m", // 绿色 + [LogLevel.WARN]: "\x1b[33m", // 黄色 + [LogLevel.ERROR]: "\x1b[31m", // 红色 + [LogLevel.FATAL]: "\x1b[35m", // 紫色 + [LogLevel.OFF]: "", // 无色 +} diff --git a/packages/logger/main-error.ts b/packages/logger/main-error.ts new file mode 100644 index 0000000..8785f62 --- /dev/null +++ b/packages/logger/main-error.ts @@ -0,0 +1,177 @@ +import { LogLevel, LogLevelName } from "./common" +import logger from "./main" + +/** + * 错误详情接口 + */ +export interface ErrorDetail { + message: string + stack?: string + componentInfo?: string + additionalInfo?: Record + timestamp: string + type: string +} + +/** + * 错误处理配置 + */ +export interface ErrorHandlerOptions { + namespace?: string + level?: LogLevel + includeStack?: boolean + includeComponentInfo?: boolean + formatError?: (error: any) => ErrorDetail +} + +/** + * 默认错误处理配置 + */ +const DEFAULT_OPTIONS: ErrorHandlerOptions = { + namespace: "error", + level: LogLevel.ERROR, + includeStack: true, + includeComponentInfo: true, +} + +/** + * 格式化错误信息 + */ +const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => { + // 如果已经是ErrorDetail格式,直接返回 + if (error && typeof error === "object" && error.type && error.message && error.timestamp) { + return error as ErrorDetail + } + + // 基本错误信息 + const errorDetail: ErrorDetail = { + message: "", + timestamp: new Date().toISOString(), + type: "Unknown", + } + + // 处理不同类型的错误 + if (error instanceof Error) { + errorDetail.message = error.message + errorDetail.type = error.name || error.constructor.name + if (options.includeStack) { + errorDetail.stack = error.stack + } + } else if (typeof error === "string") { + errorDetail.message = error + errorDetail.type = "String" + } else if (error === null) { + errorDetail.message = "Null error received" + errorDetail.type = "Null" + } else if (error === undefined) { + errorDetail.message = "Undefined error received" + errorDetail.type = "Undefined" + } else if (typeof error === "object") { + try { + errorDetail.message = error.message || JSON.stringify(error) + errorDetail.type = "Object" + errorDetail.additionalInfo = { ...error } + } catch (e) { + errorDetail.message = "Unserializable error object" + errorDetail.type = "Unserializable" + } + } else { + try { + errorDetail.message = String(error) + errorDetail.type = typeof error + } catch (e) { + errorDetail.message = "Error converting to string" + errorDetail.type = "Unknown" + } + } + + return errorDetail +} + +/** + * 错误处理类 + */ +export class ErrorHandler { + private options: ErrorHandlerOptions + + constructor(options?: Partial) { + this.options = { ...DEFAULT_OPTIONS, ...options } + } + + /** + * 记录错误 + */ + public captureError(error: any, componentInfo?: string, additionalInfo?: Record): void { + const errorDetail = formatError(error, this.options) + + // 添加组件信息 + if (this.options.includeComponentInfo && componentInfo) { + errorDetail.componentInfo = componentInfo + } + + // 添加额外信息 + if (additionalInfo) { + errorDetail.additionalInfo = { + ...errorDetail.additionalInfo, + ...additionalInfo, + } + } + + // 使用logger记录错误 + const namespace = this.options.namespace || "error" + const level = LogLevelName[this.options.level || LogLevel.ERROR].toLowerCase() + + // 构建错误消息 + let errorMessage = `${errorDetail.type}: ${errorDetail.message}` + if (errorDetail.componentInfo) { + errorMessage += ` | Component: ${errorDetail.componentInfo}` + } + + // 记录错误 + logger[level](namespace, errorMessage) + + // 如果有堆栈信息,单独记录 + if (errorDetail.stack) { + logger[level](namespace, `Stack: ${errorDetail.stack}`) + } + + // 如果有额外信息,单独记录 + if (errorDetail.additionalInfo) { + try { + const additionalInfoStr = JSON.stringify(errorDetail.additionalInfo, null, 2) + logger[level](namespace, `Additional Info: ${additionalInfoStr}`) + } catch (e) { + logger[level](namespace, "Additional Info: [Unserializable]") + } + } + } + + /** + * 设置错误处理选项 + */ + public setOptions(options: Partial): void { + this.options = { ...this.options, ...options } + } + + /** + * 获取当前选项 + */ + public getOptions(): ErrorHandlerOptions { + return { ...this.options } + } +} + +// 创建默认实例 +const errorHandler = new ErrorHandler() + +// 捕获未处理的Promise异常 +process.on("unhandledRejection", reason => { + errorHandler.captureError(reason) +}) + +// 捕获未捕获的异常 +process.on("uncaughtException", error => { + errorHandler.captureError(error) +}) + +export default errorHandler diff --git a/packages/logger/main.ts b/packages/logger/main.ts index 3851385..12bbbda 100644 --- a/packages/logger/main.ts +++ b/packages/logger/main.ts @@ -2,29 +2,8 @@ import { app, ipcMain } from "electron" import fs from "fs" import path from "path" import * as rfs from "rotating-file-stream" -import { LogLevel } from "./common" - -// 日志级别名称映射 -const LogLevelName: Record = { - [LogLevel.TRACE]: "TRACE", - [LogLevel.DEBUG]: "DEBUG", - [LogLevel.INFO]: "INFO", - [LogLevel.WARN]: "WARN", - [LogLevel.ERROR]: "ERROR", - [LogLevel.FATAL]: "FATAL", - [LogLevel.OFF]: "OFF", -} +import { LogLevel, LogLevelColor, LogLevelName } from "./common" -// 日志颜色映射(控制台输出用) -const LogLevelColor: Record = { - [LogLevel.TRACE]: "\x1b[90m", // 灰色 - [LogLevel.DEBUG]: "\x1b[36m", // 青色 - [LogLevel.INFO]: "\x1b[32m", // 绿色 - [LogLevel.WARN]: "\x1b[33m", // 黄色 - [LogLevel.ERROR]: "\x1b[31m", // 红色 - [LogLevel.FATAL]: "\x1b[35m", // 紫色 - [LogLevel.OFF]: "", // 无色 -} // 重置颜色的ANSI代码 const RESET_COLOR = "\x1b[0m" diff --git a/packages/logger/preload-error.ts b/packages/logger/preload-error.ts new file mode 100644 index 0000000..e07e927 --- /dev/null +++ b/packages/logger/preload-error.ts @@ -0,0 +1,213 @@ +import { contextBridge, ipcRenderer } from "electron" +import { LogLevel, LogLevelName } from "./common" +import logger from "./preload" + +/** + * 错误详情接口 + */ +export interface ErrorDetail { + message: string + stack?: string + componentInfo?: string + additionalInfo?: Record + timestamp: string + type: string +} + +/** + * 错误处理配置 + */ +export interface ErrorHandlerOptions { + namespace?: string + level?: LogLevel + includeStack?: boolean + includeComponentInfo?: boolean +} + +/** + * 渲染进程错误处理接口 + */ +interface IRendererErrorHandler { + /** + * 捕获错误 + */ + captureError(error: any, componentInfo?: string, additionalInfo?: Record): void + + /** + * 设置错误处理选项 + */ + setOptions(options: Partial): void + + /** + * 获取当前选项 + */ + getOptions(): ErrorHandlerOptions + + /** + * 安装全局错误处理器 + */ + installGlobalHandlers(): void +} + +/** + * 默认错误处理配置 + */ +const DEFAULT_OPTIONS: ErrorHandlerOptions = { + namespace: "error", + level: LogLevel.ERROR, + includeStack: true, + includeComponentInfo: true, +} + +/** + * 格式化错误信息 + */ +const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => { + // 基本错误信息 + const errorDetail: ErrorDetail = { + message: "", + timestamp: new Date().toISOString(), + type: "Unknown", + } + // 处理不同类型的错误 + if (error instanceof Error) { + errorDetail.message = error.message + errorDetail.type = error.name || error.constructor.name + if (options.includeStack) { + errorDetail.stack = error.stack + } + } else if (typeof error === "string") { + errorDetail.message = error + errorDetail.type = "String" + } else if (error === null) { + errorDetail.message = "Null error received" + errorDetail.type = "Null" + } else if (error === undefined) { + errorDetail.message = "Undefined error received" + errorDetail.type = "Undefined" + } else if (typeof error === "object") { + try { + errorDetail.message = error.message || JSON.stringify(error) + errorDetail.type = "Object" + errorDetail.additionalInfo = { ...error } + } catch (e) { + errorDetail.message = "Unserializable error object" + errorDetail.type = "Unserializable" + } + } else { + try { + errorDetail.message = String(error) + errorDetail.type = typeof error + } catch (e) { + errorDetail.message = "Error converting to string" + errorDetail.type = "Unknown" + } + } + + return errorDetail +} + +/** + * 创建渲染进程错误处理器 + */ +const createRendererErrorHandler = (): IRendererErrorHandler => { + // 当前错误处理选项 + let options: ErrorHandlerOptions = { ...DEFAULT_OPTIONS } + + /** + * 发送错误到主进程 + */ + const sendError = (error: any, componentInfo?: string, additionalInfo?: Record) => { + const errorDetail = formatError(error, options) + + // 添加组件信息 + if (options.includeComponentInfo && componentInfo) { + errorDetail.componentInfo = componentInfo + } + + // 使用logger记录错误 + const namespace = options.namespace || "error" + const level = LogLevelName[options.level || LogLevel.ERROR].toLowerCase() + + // 添加额外信息 + if (additionalInfo) { + errorDetail.additionalInfo = { + ...errorDetail.additionalInfo, + ...additionalInfo, + } + } + + // 通过logger直接记录错误,不再使用IPC通信 + logger[level](namespace, JSON.stringify(errorDetail)) + + // 同时在控制台输出错误信息 + logger[level](namespace, `${errorDetail.type}: ${errorDetail.message}`) + if (errorDetail.stack) { + logger[level](namespace, `Stack: ${errorDetail.stack}`) + } + } + + /** + * 安装全局错误处理器 + */ + const installGlobalHandlers = () => { + // 捕获未处理的异常 + window.addEventListener("error", event => { + event.preventDefault() + sendError(event.error || event.message, "window.onerror", { + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }) + return true + }) + + // 捕获未处理的Promise拒绝 + window.addEventListener("unhandledrejection", event => { + event.preventDefault() + sendError(event.reason, "unhandledrejection", { + promise: "[Promise]", // 不能直接序列化Promise对象 + }) + return true + }) + + // 捕获资源加载错误 + document.addEventListener( + "error", + event => { + // 只处理资源加载错误 + if (event.target && (event.target as HTMLElement).tagName) { + const target = event.target as HTMLElement + sendError(`Resource load failed: ${(target as any).src || (target as any).href}`, "resource.error", { + tagName: target.tagName, + src: (target as any).src, + href: (target as any).href, + }) + } + }, + true, + ) // 使用捕获阶段 + + logger.info("[ErrorHandler] Global error handlers installed") + } + + return { + captureError: sendError, + setOptions: (newOptions: Partial) => { + options = { ...options, ...newOptions } + // 同步选项到主进程 + ipcRenderer.send("logger:errorOptions", options) + }, + getOptions: () => ({ ...options }), + installGlobalHandlers, + } +} + +const errorHandler = createRendererErrorHandler() +errorHandler.installGlobalHandlers() + +// 暴露错误处理器到渲染进程全局 +contextBridge.exposeInMainWorld("errorHandler", errorHandler) + +// 导出类型定义,方便在渲染进程中使用 +export type { IRendererErrorHandler } diff --git a/packages/logger/preload.ts b/packages/logger/preload.ts index 17a8ea7..e249ddc 100644 --- a/packages/logger/preload.ts +++ b/packages/logger/preload.ts @@ -62,16 +62,18 @@ const createRendererLogger = (): IRendererLogger => { // 格式化消息 const formatMessages = (messages: any[]): string => { - return messages.map(msg => { - if (typeof msg === "object") { - try { - return JSON.stringify(msg) - } catch (e) { - return String(msg) + return messages + .map(msg => { + if (typeof msg === "object") { + try { + return JSON.stringify(msg) + } catch (e) { + return String(msg) + } } - } - return String(msg) - }).join(" ") + return String(msg) + }) + .join(" ") } // 本地打印日志 @@ -134,14 +136,18 @@ const createRendererLogger = (): IRendererLogger => { setLevel: (level: LogLevel) => { currentLevel = level ipcRenderer.send("logger:setLevel", level) - } + }, } }, } } +const logger = createRendererLogger() + // 暴露logger对象到渲染进程全局 -contextBridge.exposeInMainWorld("logger", createRendererLogger()) +contextBridge.exposeInMainWorld("logger", logger) +export { logger } +export default logger // 导出类型定义,方便在渲染进程中使用 export type { IRendererLogger } diff --git a/packages/setting/main.ts b/packages/setting/main.ts index 9ef038b..19c725c 100644 --- a/packages/setting/main.ts +++ b/packages/setting/main.ts @@ -5,7 +5,6 @@ import { cloneDeep } from "lodash" import Config from "config" import type { IDefaultConfig } from "config" import _debug from "debug" -import logger from "logger/main" const debug = _debug("app:setting") @@ -60,7 +59,6 @@ function isEmptyDir(fPath: string) { class SettingClass { constructor() { - logger.info("setting", "aaaa") debug(`Setting inited`) this.init() } diff --git a/src/main/index.ts b/src/main/index.ts index 0931bad..e3eae03 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,5 +1,6 @@ import "reflect-metadata" import "logger/main" +import "logger/main-error" import "setting/main" import { _ioc } from "main/_ioc" diff --git a/src/preload/index.ts b/src/preload/index.ts index 33cb5cf..58b2883 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron" import { electronAPI } from "@electron-toolkit/preload" import { call, callLong, callSync } from "./call" import "logger/preload" +import "logger/preload-error" import { IPopupMenuOption } from "#/popup-menu" document.addEventListener("DOMContentLoaded", () => { const initStyle = document.createElement("style") diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index f113113..75eb741 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -1,13 +1,14 @@