Compare commits

...

4 Commits

Author SHA1 Message Date
npmrun c142937af9 refactor(logger): 重构错误处理逻辑,分离渲染进程和preload进程的错误处理 3 weeks ago
npmrun 950bfe9060 feat(logger): 添加错误处理模块并集成到主进程和渲染进程 3 weeks ago
npmrun 7035429775 feat(logger): 添加固定命名空间的日志记录功能 3 weeks ago
npmrun 05f83e2a08 feat: 添加logger和setting模块并重构日志系统 3 weeks ago
  1. 2
      package.json
  2. 31
      packages/logger/common.ts
  3. 177
      packages/logger/main-error.ts
  4. 271
      packages/logger/main.ts
  5. 7
      packages/logger/package.json
  6. 195
      packages/logger/preload-error.ts
  7. 153
      packages/logger/preload.ts
  8. 243
      packages/logger/renderer-error.ts
  9. 12
      packages/setting/main.ts
  10. 7
      packages/setting/package.json
  11. 10
      pnpm-lock.yaml
  12. 2
      src/main/App.ts
  13. 78
      src/main/index.ts
  14. 3
      src/main/modules/_ioc.ts
  15. 8
      src/main/modules/db/index.ts
  16. 2
      src/preload/index.ts
  17. 7
      src/renderer/src/App.vue
  18. 7
      src/renderer/src/main.ts
  19. 5
      src/types/errorHandler.d.ts
  20. 5
      src/types/logger.d.ts
  21. 13
      tsconfig.node.json
  22. 9
      tsconfig.web.json

2
package.json

@ -56,10 +56,12 @@
"extract-zip": "^2.0.1",
"locales": "workspace:*",
"lodash-es": "^4.17.21",
"logger": "workspace:^",
"monaco-editor": "^0.52.2",
"prettier": "^3.5.1",
"rotating-file-stream": "^3.2.6",
"sass": "^1.85.0",
"setting": "workspace:^",
"simplebar-vue": "^2.4.0",
"typescript": "^5.7.3",
"unocss": "^0.64.1",

31
packages/logger/common.ts

@ -0,0 +1,31 @@
// 日志级别定义
export enum LogLevel {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
FATAL = 5,
OFF = 6,
}
// 日志级别名称映射
export const LogLevelName: Record<LogLevel, string> = {
[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, string> = {
[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]: "", // 无色
}

177
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<string, any>
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<ErrorHandlerOptions>) {
this.options = { ...DEFAULT_OPTIONS, ...options }
}
/**
*
*/
public captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): 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<ErrorHandlerOptions>): 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

271
packages/logger/main.ts

@ -0,0 +1,271 @@
import { app, ipcMain } from "electron"
import fs from "fs"
import path from "path"
import * as rfs from "rotating-file-stream"
import { LogLevel, LogLevelColor, LogLevelName } from "./common"
// 重置颜色的ANSI代码
const RESET_COLOR = "\x1b[0m"
// 日志配置接口
export interface LoggerOptions {
level?: LogLevel // 日志级别
namespace?: string // 日志命名空间
console?: boolean // 是否输出到控制台
file?: boolean // 是否输出到文件
maxSize?: string // 单个日志文件最大大小
maxFiles?: number // 保留的最大日志文件数量
}
// 默认配置
const DEFAULT_OPTIONS: LoggerOptions = {
level: LogLevel.INFO,
namespace: "app",
console: true,
file: true,
maxSize: "10M",
maxFiles: 10,
}
let logDir
const isElectronApp = !!process.versions.electron
if (isElectronApp && app) {
logDir = path.join(app.getPath("logs"))
} else {
// 非Electron环境下使用当前目录下的logs文件夹
logDir = path.join(process.cwd(), "logs")
}
/**
*
*/
export class Logger {
private static instance: Logger
private options: LoggerOptions = DEFAULT_OPTIONS
private logStream: rfs.RotatingFileStream | null = null
private logDir: string = logDir
private currentLogFile: string = ""
private isElectronApp: boolean = !!process.versions.electron
private callInitialize: boolean = false
/**
*
*/
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger()
}
if (Logger.instance.callInitialize) {
return Logger.instance
} else {
// 创建代理对象,确保只有在初始化后才能访问除init之外的方法
const handler = {
get: function (target: any, prop: string) {
if (prop === "init") {
return target[prop]
}
if (!target.callInitialize) {
throw new Error(`Logger未初始化,不能调用${prop}方法,请先调用init()方法`)
}
return target[prop]
},
}
Logger.instance = new Proxy(new Logger(), handler)
}
return Logger.instance
}
/**
*
*/
private constructor() {}
public init(options?: LoggerOptions): void {
this.callInitialize = true
this.options = { ...this.options, ...options }
// 确保日志目录存在
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true })
}
// 初始化日志文件
this.initLogFile()
// 如果在主进程中,设置IPC监听器接收渲染进程的日志
if (this.isElectronApp && process.type === "browser") {
this.setupIPC()
}
}
/**
*
*/
private initLogFile(): void {
if (!this.options.file) return
// 生成日志文件名
const now = new Date()
const timestamp = now.toISOString().replace(/[:.]/g, "-")
this.currentLogFile = `app-logger-${timestamp}.log`
// 创建日志流
this.logStream = rfs.createStream(this.currentLogFile, {
path: this.logDir,
size: this.options.maxSize,
rotate: this.options.maxFiles,
})
}
/**
* IPC通信
*/
private setupIPC(): void {
if (!ipcMain) return
ipcMain.on("logger:log", (_, level: LogLevel, namespace: string, ...messages: any[]) => {
this.logWithLevel(level, namespace, ...messages)
})
// 处理日志级别设置请求
ipcMain.on("logger:setLevel", (_, level: LogLevel) => {
this.setLevel(level)
})
}
/**
*
*/
public close(): void {
if (this.logStream) {
this.logStream.end()
this.logStream.destroy()
this.logStream = null
}
}
/**
*
*/
public setLevel(level: LogLevel): void {
this.options.level = level
}
/**
*
*/
public getLevel(): LogLevel {
return this.options.level || LogLevel.INFO
}
/**
*
*/
private logWithLevel(level: LogLevel, namespace: string, ...messages: any[]): void {
// 检查日志级别
if (level < this.getLevel() || level === LogLevel.OFF) return
const timestamp = new Date().toISOString()
const levelName = LogLevelName[level]
const prefix = `[${timestamp}] [${namespace}] [${levelName}]`
// 格式化消息
const formattedMessages = messages.map(msg => {
if (typeof msg === "object") {
try {
return JSON.stringify(msg)
} catch (e) {
return String(msg)
}
}
return String(msg)
})
const message = formattedMessages.join(" ")
// 输出到控制台
if (this.options.console) {
const color = LogLevelColor[level]
console.log(`${color}${prefix} ${message}${RESET_COLOR}`)
}
// 写入日志文件
if (this.options.file && this.logStream) {
this.logStream.write(`${prefix} ${message}\n`)
}
}
/**
*
*/
public trace(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.TRACE, namespace, ...messages)
}
/**
*
*/
public debug(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.DEBUG, namespace, ...messages)
}
/**
*
*/
public info(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.INFO, namespace, ...messages)
}
/**
*
*/
public warn(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.WARN, namespace, ...messages)
}
/**
*
*/
public error(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.ERROR, namespace, ...messages)
}
/**
*
*/
public fatal(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.FATAL, namespace, ...messages)
}
/**
*
* @param namespace
* @returns
*/
public createNamespace(namespace: string) {
return {
trace: (...messages: any[]) => this.trace(namespace, ...messages),
debug: (...messages: any[]) => this.debug(namespace, ...messages),
info: (...messages: any[]) => this.info(namespace, ...messages),
warn: (...messages: any[]) => this.warn(namespace, ...messages),
error: (...messages: any[]) => this.error(namespace, ...messages),
fatal: (...messages: any[]) => this.fatal(namespace, ...messages),
setLevel: (level: LogLevel) => this.setLevel(level),
getLevel: () => this.getLevel(),
}
}
}
// 默认实例
const logger = Logger.getInstance()
logger.init()
// 应用退出时关闭日志流
if (process.type === "browser" && app) {
app.on("before-quit", () => {
logger.info("app", "应用关闭")
logger.close()
})
}
export default logger

7
packages/logger/package.json

@ -0,0 +1,7 @@
{
"name": "logger",
"version": "1.0.0",
"keywords": [],
"author": "",
"license": "ISC"
}

195
packages/logger/preload-error.ts

@ -0,0 +1,195 @@
import { contextBridge, ipcRenderer } from "electron"
import { LogLevel, LogLevelName } from "./common"
import logger from "./preload"
/**
*
*/
interface ErrorDetail {
message: string
stack?: string
componentInfo?: string
additionalInfo?: Record<string, any>
timestamp: string
type: string
}
/**
*
*/
interface ErrorHandlerOptions {
namespace?: string
level?: LogLevel
includeStack?: boolean
includeComponentInfo?: boolean
}
/**
*
*/
interface IRendererErrorHandler {
/**
*
*/
captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void
/**
*
*/
setOptions(options: Partial<ErrorHandlerOptions>): 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 handleError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => {
// 如果已经是ErrorDetail格式,直接使用
let errorDetail: ErrorDetail
if (error && typeof error === "object" && error.type && error.message && error.timestamp) {
errorDetail = error as ErrorDetail
} else {
// 否则格式化错误
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[level](namespace, JSON.stringify(errorDetail))
// 同时在控制台输出错误信息
logger[level](namespace, `${errorDetail.type}: ${errorDetail.message}`)
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]")
}
}
}
/**
*
* renderer-error.ts负责
*/
const installGlobalHandlers = () => {
// 不再在preload层安装全局错误处理器
// 仅记录日志表明该方法被调用
logger.info("[ErrorHandler] Global error handlers should be installed in renderer process")
}
return {
captureError: handleError,
setOptions: (newOptions: Partial<ErrorHandlerOptions>) => {
options = { ...options, ...newOptions }
// 同步选项到主进程
ipcRenderer.send("logger:errorOptions", options)
},
getOptions: () => ({ ...options }),
installGlobalHandlers,
}
}
const errorHandler = createRendererErrorHandler()
// 暴露错误处理器到渲染进程全局
contextBridge.exposeInMainWorld("preloadErrorHandler", errorHandler)
// 导出类型定义,方便在渲染进程中使用
export type { IRendererErrorHandler, ErrorDetail, ErrorHandlerOptions }

153
packages/logger/preload.ts

@ -0,0 +1,153 @@
import { contextBridge, ipcRenderer } from "electron"
import { LogLevel } from "./common"
/**
*
*/
interface IRendererLogger {
trace(namespace: string, ...messages: any[]): void
debug(namespace: string, ...messages: any[]): void
info(namespace: string, ...messages: any[]): void
warn(namespace: string, ...messages: any[]): void
error(namespace: string, ...messages: any[]): void
fatal(namespace: string, ...messages: any[]): void
setLevel(level: LogLevel): void
createNamespace(namespace: string): INamespacedLogger
}
/**
*
*/
interface INamespacedLogger {
trace(...messages: any[]): void
debug(...messages: any[]): void
info(...messages: any[]): void
warn(...messages: any[]): void
error(...messages: any[]): void
fatal(...messages: any[]): void
setLevel(level: LogLevel): void
}
// 日志级别名称映射
const LogLevelName: Record<LogLevel, string> = {
[LogLevel.TRACE]: "TRACE",
[LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO",
[LogLevel.WARN]: "WARN",
[LogLevel.ERROR]: "ERROR",
[LogLevel.FATAL]: "FATAL",
[LogLevel.OFF]: "OFF",
}
// 日志颜色映射(控制台输出用)
const LogLevelColor: Record<LogLevel, string> = {
[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"
/**
*
*/
const createRendererLogger = (): IRendererLogger => {
// 当前日志级别
let currentLevel: LogLevel = LogLevel.INFO
// 格式化消息
const formatMessages = (messages: any[]): string => {
return messages
.map(msg => {
if (typeof msg === "object") {
try {
return JSON.stringify(msg)
} catch (e) {
return String(msg)
}
}
return String(msg)
})
.join(" ")
}
// 本地打印日志
const printLog = (level: LogLevel, namespace: string, ...messages: any[]): void => {
// 检查日志级别
if (level < currentLevel || level === LogLevel.OFF) return
const timestamp = new Date().toISOString()
const levelName = LogLevelName[level]
const prefix = `[${timestamp}] [${namespace}] [${levelName}]`
const message = formatMessages(messages)
// 输出到控制台
const color = LogLevelColor[level]
console.log(`${color}${prefix} ${message}${RESET_COLOR}`)
}
// 通过IPC发送日志到主进程
const sendLog = (level: LogLevel, namespace: string, ...messages: any[]) => {
// 本地打印
printLog(level, namespace, ...messages)
// 发送到主进程
ipcRenderer.send("logger:log", level, namespace, ...messages)
}
return {
trace(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.TRACE, namespace, ...messages)
},
debug(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.DEBUG, namespace, ...messages)
},
info(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.INFO, namespace, ...messages)
},
warn(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.WARN, namespace, ...messages)
},
error(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.ERROR, namespace, ...messages)
},
fatal(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.FATAL, namespace, ...messages)
},
setLevel(level: LogLevel): void {
// 更新本地日志级别
currentLevel = level
// 设置日志级别(可选,如果需要在渲染进程中动态调整日志级别)
ipcRenderer.send("logger:setLevel", level)
},
createNamespace(namespace: string): INamespacedLogger {
return {
trace: (...messages: any[]) => sendLog(LogLevel.TRACE, namespace, ...messages),
debug: (...messages: any[]) => sendLog(LogLevel.DEBUG, namespace, ...messages),
info: (...messages: any[]) => sendLog(LogLevel.INFO, namespace, ...messages),
warn: (...messages: any[]) => sendLog(LogLevel.WARN, namespace, ...messages),
error: (...messages: any[]) => sendLog(LogLevel.ERROR, namespace, ...messages),
fatal: (...messages: any[]) => sendLog(LogLevel.FATAL, namespace, ...messages),
setLevel: (level: LogLevel) => {
currentLevel = level
ipcRenderer.send("logger:setLevel", level)
},
}
},
}
}
const logger = createRendererLogger()
// 暴露logger对象到渲染进程全局
contextBridge.exposeInMainWorld("logger", logger)
export { logger }
export default logger
// 导出类型定义,方便在渲染进程中使用
export type { IRendererLogger }

243
packages/logger/renderer-error.ts

@ -0,0 +1,243 @@
import { LogLevel, LogLevelName } from "./common"
/**
*
*/
interface ErrorDetail {
message: string
stack?: string
componentInfo?: string
additionalInfo?: Record<string, any>
timestamp: string
type: string
}
/**
*
*/
interface ErrorHandlerOptions {
namespace?: string
level?: LogLevel
includeStack?: boolean
includeComponentInfo?: boolean
}
/**
*
*/
export interface IRendererErrorHandler {
/**
*
*/
captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void
/**
*
*/
setOptions(options: Partial<ErrorHandlerOptions>): 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",
}
console.log(error)
// 处理不同类型的错误
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
}
// @ts-ignore
const preloadErrorHandler = window.preloadErrorHandler
/**
*
*/
export const createRendererErrorHandler = (): IRendererErrorHandler => {
// 当前错误处理选项
let options: ErrorHandlerOptions = { ...DEFAULT_OPTIONS }
/**
*
*/
const processError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>): ErrorDetail => {
const errorDetail = formatError(error, options)
// 添加组件信息
if (options.includeComponentInfo && componentInfo) {
errorDetail.componentInfo = componentInfo
}
// 添加额外信息
if (additionalInfo) {
errorDetail.additionalInfo = {
...errorDetail.additionalInfo,
...additionalInfo,
}
}
return errorDetail
}
/**
* preload层
*/
const sendError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => {
// 处理并序列化错误
const errorDetail = processError(error, componentInfo, additionalInfo)
// 调用window.errorHandler.captureError发送错误
// 这里假设preload层已经暴露了errorHandler对象
if (preloadErrorHandler && typeof preloadErrorHandler.captureError === "function") {
preloadErrorHandler.captureError(errorDetail)
} else {
// 如果errorHandler不可用,则降级到控制台输出
console.error("[ErrorHandler]", errorDetail)
}
}
/**
*
*/
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,
) // 使用捕获阶段
console.info("[ErrorHandler] Global error handlers installed")
}
return {
captureError: sendError,
setOptions: (newOptions: Partial<ErrorHandlerOptions>) => {
options = { ...options, ...newOptions }
// 同步选项到preload层
if (preloadErrorHandler && typeof preloadErrorHandler.setOptions === "function") {
preloadErrorHandler.setOptions(options)
}
},
getOptions: () => ({ ...options }),
installGlobalHandlers,
}
}
// 导出类型定义,方便在渲染进程中使用
export type { ErrorDetail, ErrorHandlerOptions }
// 创建渲染进程错误处理器
const errorHandler = createRendererErrorHandler()
// 安装全局错误处理器
errorHandler.installGlobalHandlers()
window.errorHandler = errorHandler
/**
* 使
*
* // 捕获特定错误
* try {
* // 可能出错的代码
* } catch (error) {
* errorHandler.captureError(error, 'ComponentName', { additionalInfo: 'value' })
* }
*
* // 设置错误处理选项
* errorHandler.setOptions({
* namespace: 'custom-error',
* includeComponentInfo: true
* })
*/

12
src/main/modules/setting/index.ts → packages/setting/main.ts

@ -2,11 +2,9 @@ import fs from "fs-extra"
import { app } from "electron"
import path from "path"
import { cloneDeep } from "lodash"
import { injectable } from "inversify"
import Config from "config"
import type { IDefaultConfig } from "config"
import _debug from "debug"
import BaseClass from "main/base/base"
const debug = _debug("app:setting")
@ -59,18 +57,12 @@ function isEmptyDir(fPath: string) {
}
}
@injectable()
class Setting extends BaseClass {
class SettingClass {
constructor() {
super()
debug(`Setting inited`)
this.init()
}
destroy() {
// TODO
}
#cb: [IT, IOnFunc][] = []
onChange(fn: IOnFunc, that?: any)
@ -232,5 +224,7 @@ class Setting extends BaseClass {
}
}
const Setting = new SettingClass()
export default Setting
export { Setting }

7
packages/setting/package.json

@ -0,0 +1,7 @@
{
"name": "setting",
"version": "1.0.0",
"keywords": [],
"author": "",
"license": "ISC"
}

10
pnpm-lock.yaml

@ -96,6 +96,9 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
logger:
specifier: workspace:^
version: link:packages/logger
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
@ -108,6 +111,9 @@ importers:
sass:
specifier: ^1.85.0
version: 1.85.0
setting:
specifier: workspace:^
version: link:packages/setting
simplebar-vue:
specifier: ^2.4.0
version: 2.4.0(vue@3.5.13(typescript@5.7.3))
@ -153,6 +159,10 @@ importers:
packages/locales: {}
packages/logger: {}
packages/setting: {}
packages:
7zip-bin@5.2.0:

2
src/main/App.ts

@ -3,7 +3,7 @@ import { inject, injectable } from "inversify"
// import DB from "./modules/db"
import Api from "./modules/api"
import WindowManager from "./modules/window-manager"
import { app, nativeTheme, protocol } from "electron"
import { app, protocol } from "electron"
import { electronApp } from "@electron-toolkit/utils"
import Command from "./modules/commands"
import BaseClass from "./base/base"

78
src/main/index.ts

@ -1,4 +1,8 @@
import "reflect-metadata"
import "logger/main"
import "logger/main-error"
import "setting/main"
import { _ioc } from "main/_ioc"
import { App } from "main/App"
@ -14,61 +18,45 @@ console.log(`日志地址:${logsPath}`)
const LOG_ROOT = path.join(logsPath)
// 缓存已创建的文件流(避免重复创建)
const streams = new Map()
// 缓存当前应用启动的日志文件流
let currentLogStream: rfs.RotatingFileStream | null = null
// 转换命名空间为安全路径
function sanitizeNamespace(namespace) {
return namespace
.split(":") // 按层级分隔符拆分
.map(part => part.replace(/[\\/:*?"<>|]/g, "_")) // 替换非法字符
.join(path.sep) // 拼接为系统路径分隔符(如 / 或 \)
// 生成当前启动时的日志文件名
const getLogFileName = () => {
const now = new Date()
const timestamp = now.toISOString().replace(/[:.]/g, '-')
return `app-${timestamp}.log`
}
// 覆盖 debug.log 方法
const originalLog = debug.log
debug.log = function (...args) {
// 保留原始控制台输出(可选)
// 保留原始控制台输出
originalLog.apply(this, args)
// 获取当前命名空间
// @ts-ignore ...
const namespace = this.namespace
if (!namespace) {
// TODO 增加容错机制,如果没有命名空间就输出到一个默认文件中
return
}
// 生成日志文件路径(示例:logs/app/server.log)
const sanitizedPath = sanitizeNamespace(namespace)
// const logFilePath = path.join(LOG_ROOT, `${sanitizedPath}.log`)
const today = new Date().toISOString().split("T")[0]
const logFilePath = path.join(LOG_ROOT, sanitizedPath, `${today}.log`)
// 确保目录存在
const dir = path.dirname(logFilePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }) // 自动创建多级目录
// 确保日志目录存在
if (!fs.existsSync(LOG_ROOT)) {
fs.mkdirSync(LOG_ROOT, { recursive: true })
}
// 获取或创建文件流
let stream = streams.get(logFilePath)
if (!stream) {
// stream = fs.createWriteStream(logFilePath, { flags: "a" }) // 追加模式
stream = rfs.createStream(path.parse(logFilePath).base, {
path: dir,
// 延迟初始化日志流,直到第一次写入
if (!currentLogStream) {
const logFileName = getLogFileName()
currentLogStream = rfs.createStream(logFileName, {
path: LOG_ROOT,
size: "10M", // 单个文件最大 10MB
rotate: 5, // 保留最近 5 个文件
rotate: 10, // 保留最近 10 个文件
})
streams.set(logFilePath, stream)
}
// 写入日志(添加时间戳)
const message = args.join(" ")
stream.write(`${message}\n`)
// 获取当前命名空间
// @ts-ignore
const namespace = this.namespace || 'unknown'
// const timestamp = new Date().toISOString()
// stream.write(`[${timestamp}] ${message}\n`)
// 写入日志(添加时间戳和命名空间)
const timestamp = new Date().toISOString()
const message = args.join(" ")
currentLogStream.write(`[${timestamp}] [${namespace}] ${message}\n`)
}
const curApp = _ioc.get(App)
@ -77,9 +65,9 @@ curApp.init()
const _debug = debug("app:app")
app.on("before-quit", () => {
_debug("应用关闭")
streams.forEach(stream => {
stream.end()
stream.destroy()
})
streams.clear()
if (currentLogStream) {
currentLogStream.end()
currentLogStream.destroy()
currentLogStream = null
}
})

3
src/main/modules/_ioc.ts

@ -1,5 +1,4 @@
import { Container, ContainerModule } from "inversify"
import { Setting } from "./setting"
import { DB } from "./db"
import { Api } from "./api"
import { WindowManager } from "./window-manager"
@ -9,7 +8,6 @@ import Zephyr from "./zephyr"
import Updater from "./updater"
const modules = new ContainerModule(bind => {
bind(Setting).toConstantValue(new Setting())
bind(Zephyr).toSelf().inSingletonScope()
bind(Updater).toSelf().inSingletonScope()
bind(Api).toSelf().inSingletonScope()
@ -21,7 +19,6 @@ const modules = new ContainerModule(bind => {
async function destroyAllModules(ioc: Container) {
await Promise.all([
ioc.get(Setting).destroy(),
ioc.get(WindowManager).destroy(),
ioc.get(Commands).destroy(),
ioc.get(Updater).destroy(),

8
src/main/modules/db/index.ts

@ -1,5 +1,5 @@
import { inject, injectable } from "inversify"
import Setting from "../setting"
import Setting from "setting/main"
import { CustomAdapter, CustomLow } from "./custom"
import path from "node:path"
import BaseClass from "main/base/base"
@ -14,7 +14,7 @@ class DB extends BaseClass {
}
Modules: Record<string, CustomLow<any>> = {}
constructor(@inject(Setting) private _setting: Setting) {
constructor() {
super()
}
@ -31,12 +31,12 @@ class DB extends BaseClass {
getDB(dbName: string) {
if (this.Modules[dbName] === undefined) {
const filepath = path.resolve(this._setting.values("storagePath"), "./db/" + dbName + ".json")
const filepath = path.resolve(Setting.values("storagePath"), "./db/" + dbName + ".json")
this.Modules[dbName] = this.create(filepath)
return this.Modules[dbName]
} else {
const cur = this.Modules[dbName]
const filepath = path.resolve(this._setting.values("storagePath"), "./db/" + dbName + ".json")
const filepath = path.resolve(Setting.values("storagePath"), "./db/" + dbName + ".json")
if (cur.filepath != filepath) {
this.Modules[dbName] = this.create(filepath)
}

2
src/preload/index.ts

@ -1,6 +1,8 @@
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")

7
src/renderer/src/App.vue

@ -1,9 +1,14 @@
<script setup lang="ts"></script>
<script setup lang="ts">
function throwAnError() {
throw new Error('Error thrown from throwAnError function');
}
</script>
<template>
<div h-full flex flex-col overflow-hidden>
<NavBar></NavBar>
<div flex-1 h-0 overflow-hidden flex flex-col>
<button @click="throwAnError">抛出错误</button>
<router-view v-slot="{ Component, route }">
<Transition name="slide-fade" mode="out-in">
<component :is="Component" :key="route.fullPath" />

7
src/renderer/src/main.ts

@ -1,3 +1,4 @@
import 'logger/renderer-error'
import "simplebar-vue/dist/simplebar.min.css"
import "@unocss/reset/normalize.css"
import "@/assets/style/_common.scss"
@ -13,8 +14,10 @@ const app = createApp(App)
// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
console.error("应用错误:", err)
console.info("错误信息:", info)
// console.error("应用错误:", err)
// console.info("错误信息:", info)
errorHandler.captureError(err)
errorHandler.captureError(info)
// 可以添加错误上报逻辑
}

5
src/types/errorHandler.d.ts

@ -0,0 +1,5 @@
declare const errorHandler: import("logger/preload-error").IRendererErrorHandler
interface Window {
errorHandler: import("logger/preload-error").IRendererErrorHandler
}

5
src/types/logger.d.ts

@ -0,0 +1,5 @@
declare const logger: import("logger/preload").IRendererLogger
interface Window {
logger: import("logger/preload").IRendererLogger
}

13
tsconfig.node.json

@ -5,8 +5,13 @@
"src/main/**/*",
"src/preload/**/*",
"config/**/*",
"src/types/**/*",
"packages/locales/main.ts",
"packages/setting/main.ts",
"packages/logger/main.ts",
"packages/logger/main-error.ts",
"packages/logger/preload.ts",
"packages/logger/preload-error.ts",
"packages/logger/common.ts",
"src/common/**/*.main.ts",
"src/common/**/main/**/*",
"src/common/**/main.ts",
@ -47,6 +52,12 @@
"locales/*": [
"packages/locales/*"
],
"setting/*": [
"packages/setting/*"
],
"logger/*": [
"packages/logger/*"
],
}
}
}

9
tsconfig.web.json

@ -7,7 +7,9 @@
"src/renderer/src/**/*",
"src/renderer/src/**/*.vue",
"packages/locales/**/*.ts",
"src/preload/*.d.ts",
"packages/setting/**/*.ts",
"packages/logger/**/*.ts",
"src/preload/**/*",
"src/types/**/*",
"config/**/*",
"./typed-router.d.ts",
@ -15,6 +17,9 @@
],
"exclude": [
"packages/locales/main.ts",
"packages/setting/main.ts",
"packages/logger/main.ts",
"packages/logger/main-error.ts",
"src/common/**/main/**/*",
"src/common/**/*.main.ts",
"src/common/**/main.ts"
@ -56,4 +61,4 @@
],
}
}
}
}

Loading…
Cancel
Save