From 05f83e2a08e539fab81dc34a4ae705197b2a9854 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Fri, 28 Mar 2025 00:07:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0logger=E5=92=8Csettin?= =?UTF-8?q?g=E6=A8=A1=E5=9D=97=E5=B9=B6=E9=87=8D=E6=9E=84=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增logger模块,提供统一的日志管理功能,支持不同日志级别和输出到文件/控制台 - 新增setting模块,用于管理应用配置,支持动态更新和持久化 - 重构主进程和渲染进程的日志系统,使用logger模块替代原有实现 - 删除原有的setting模块实现,使用新的setting模块替代 --- package.json | 2 + packages/logger/common.ts | 10 ++ packages/logger/main.ts | 274 ++++++++++++++++++++++++++++++++++++++ packages/logger/package.json | 7 + packages/logger/preload.ts | 119 +++++++++++++++++ packages/setting/main.ts | 232 ++++++++++++++++++++++++++++++++ packages/setting/package.json | 7 + pnpm-lock.yaml | 10 ++ src/main/App.ts | 2 +- src/main/index.ts | 77 +++++------ src/main/modules/_ioc.ts | 3 - src/main/modules/db/index.ts | 8 +- src/main/modules/setting/index.ts | 236 -------------------------------- src/preload/index.ts | 1 + src/renderer/src/App.vue | 6 +- src/types/logger.d.ts | 5 + tsconfig.node.json | 11 +- tsconfig.web.json | 6 +- 18 files changed, 724 insertions(+), 292 deletions(-) create mode 100644 packages/logger/common.ts create mode 100644 packages/logger/main.ts create mode 100644 packages/logger/package.json create mode 100644 packages/logger/preload.ts create mode 100644 packages/setting/main.ts create mode 100644 packages/setting/package.json delete mode 100644 src/main/modules/setting/index.ts create mode 100644 src/types/logger.d.ts diff --git a/package.json b/package.json index aaa00a7..9701b9e 100644 --- a/package.json +++ b/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", diff --git a/packages/logger/common.ts b/packages/logger/common.ts new file mode 100644 index 0000000..08ab7e1 --- /dev/null +++ b/packages/logger/common.ts @@ -0,0 +1,10 @@ +// 日志级别定义 +export enum LogLevel { + TRACE = 0, + DEBUG = 1, + INFO = 2, + WARN = 3, + ERROR = 4, + FATAL = 5, + OFF = 6, +} diff --git a/packages/logger/main.ts b/packages/logger/main.ts new file mode 100644 index 0000000..4f0202c --- /dev/null +++ b/packages/logger/main.ts @@ -0,0 +1,274 @@ +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, 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" + +// 日志配置接口 +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) + } +} + +// 默认实例 +const logger = Logger.getInstance() +logger.init() + +// 应用退出时关闭日志流 +if (process.type === "browser" && app) { + app.on("before-quit", () => { + logger.info("app", "应用关闭") + logger.close() + }) +} + +export default logger diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..6e5d9ce --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,7 @@ +{ + "name": "logger", + "version": "1.0.0", + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/logger/preload.ts b/packages/logger/preload.ts new file mode 100644 index 0000000..8a39788 --- /dev/null +++ b/packages/logger/preload.ts @@ -0,0 +1,119 @@ +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 +} + +// 日志级别名称映射 +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) + }, + } +} + +// 暴露logger对象到渲染进程全局 +contextBridge.exposeInMainWorld("logger", createRendererLogger()) + +// 导出类型定义,方便在渲染进程中使用 +export type { IRendererLogger } diff --git a/packages/setting/main.ts b/packages/setting/main.ts new file mode 100644 index 0000000..9ef038b --- /dev/null +++ b/packages/setting/main.ts @@ -0,0 +1,232 @@ +import fs from "fs-extra" +import { app } from "electron" +import path from "path" +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") + +type IConfig = IDefaultConfig + +type IOnFunc = (n: IConfig, c: IConfig, keys?: (keyof IConfig)[]) => void +type IT = (keyof IConfig)[] | keyof IConfig | "_" + +let storagePath = path.join(app.getPath("documents"), Config.app_title) +const storagePathDev = path.join(app.getPath("documents"), Config.app_title + "-dev") + +if (process.env.NODE_ENV === "development") { + storagePath = storagePathDev +} + +const _tempConfig = cloneDeep(Config.default_config as IConfig) +Object.keys(_tempConfig).forEach(key => { + if (typeof _tempConfig[key] === "string" && _tempConfig[key].includes("$storagePath$")) { + _tempConfig[key] = _tempConfig[key].replace(/\$storagePath\$/g, storagePath) + if (_tempConfig[key] && path.isAbsolute(_tempConfig[key])) { + _tempConfig[key] = path.normalize(_tempConfig[key]) + } + } +}) + +function isPath(str) { + // 使用正则表达式检查字符串是否以斜杠或盘符开头 + return /^(?:\/|[a-zA-Z]:\\)/.test(str) +} + +function init(config: IConfig) { + // 在配置初始化后执行 + Object.keys(config).forEach(key => { + if (config[key] && isPath(config[key]) && path.isAbsolute(config[key])) { + fs.ensureDirSync(config[key]) + } + }) + // 在配置初始化后执行 + // fs.ensureDirSync(config["snippet.storagePath"]) + // fs.ensureDirSync(config["bookmark.storagePath"]) +} + +// 判断是否是空文件夹 +function isEmptyDir(fPath: string) { + const pa = fs.readdirSync(fPath) + if (pa.length === 0) { + return true + } else { + return false + } +} + +class SettingClass { + constructor() { + logger.info("setting", "aaaa") + debug(`Setting inited`) + this.init() + } + + #cb: [IT, IOnFunc][] = [] + + onChange(fn: IOnFunc, that?: any) + onChange(key: IT, fn: IOnFunc, that?: any) + onChange(fnOrType: IT | IOnFunc, fnOrThat: IOnFunc | any = null, that: any = null) { + if (typeof fnOrType === "function") { + this.#cb.push(["_", fnOrType.bind(fnOrThat)]) + } else { + this.#cb.push([fnOrType, fnOrThat.bind(that)]) + } + } + + #runCB(n: IConfig, c: IConfig, keys: (keyof IConfig)[]) { + for (let i = 0; i < this.#cb.length; i++) { + const temp = this.#cb[i] + const k = temp[0] + const fn = temp[1] + if (k === "_") { + fn(n, c, keys) + } + if (typeof k === "string" && keys.includes(k as keyof IConfig)) { + fn(n, c) + } + if (Array.isArray(k) && k.filter(v => keys.indexOf(v) !== -1).length) { + fn(n, c) + } + } + } + + #pathFile: string = + process.env.NODE_ENV === "development" + ? path.resolve(app.getPath("userData"), "./config_path-dev") + : path.resolve(app.getPath("userData"), "./config_path") + #config: IConfig = cloneDeep(_tempConfig) + #configPath(storagePath?: string): string { + return path.join(storagePath || this.#config.storagePath, "./config.json") + } + /** + * 读取配置文件变量同步 + * @param confingPath 配置文件路径 + */ + #syncVar(confingPath?: string) { + const configFile = this.#configPath(confingPath) + if (!fs.pathExistsSync(configFile)) { + fs.ensureFileSync(configFile) + fs.writeJSONSync(configFile, {}) + } + const config = fs.readJSONSync(configFile) as IConfig + confingPath && (config.storagePath = confingPath) + // 优先取本地的值 + for (const key in config) { + // if (Object.prototype.hasOwnProperty.call(this.#config, key)) { + // this.#config[key] = config[key] || this.#config[key] + // } + // 删除配置时本地的配置不会改变,想一下哪种方式更好 + this.#config[key] = config[key] || this.#config[key] + } + } + init() { + debug(`位置:${this.#pathFile}`) + + if (fs.pathExistsSync(this.#pathFile)) { + const confingPath = fs.readFileSync(this.#pathFile, { encoding: "utf8" }) + if (confingPath && fs.pathExistsSync(this.#configPath(confingPath))) { + this.#syncVar(confingPath) + // 防止增加了配置本地却没变的情况 + this.#sync(confingPath) + } else { + this.#syncVar(confingPath) + this.#sync(confingPath) + } + } else { + this.#syncVar() + this.#sync() + } + init.call(this, this.#config) + } + config() { + return this.#config + } + #sync(c?: string) { + const config = cloneDeep(this.#config) + delete config.storagePath + const p = this.#configPath(c) + fs.ensureFileSync(p) + fs.writeJSONSync(this.#configPath(c), config) + } + #change(p: string) { + const storagePath = this.#config.storagePath + if (fs.existsSync(storagePath) && !fs.existsSync(p)) { + fs.moveSync(storagePath, p) + } + if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) { + fs.moveSync(storagePath, p, { overwrite: true }) + } + fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" }) + } + reset(key: keyof IConfig) { + this.set(key, cloneDeep(_tempConfig[key])) + } + set(key: keyof IConfig | Partial<IConfig>, value?: any) { + const oldMainConfig = Object.assign({}, this.#config) + let isChange = false + const changeKeys: (keyof IConfig)[] = [] + const canChangeStorage = (targetPath: string) => { + if (fs.existsSync(oldMainConfig.storagePath) && fs.existsSync(targetPath) && !isEmptyDir(targetPath)) { + if (fs.existsSync(path.join(targetPath, "./config.json"))) { + return true + } + return false + } + return true + } + if (typeof key === "string") { + if (value != undefined && value !== this.#config[key]) { + if (key === "storagePath") { + if (!canChangeStorage(value)) { + throw "无法改变存储地址" + return + } + this.#change(value) + changeKeys.push("storagePath") + this.#config["storagePath"] = value + } else { + changeKeys.push(key) + this.#config[key as string] = value + } + isChange = true + } + } else { + if (key["storagePath"] !== undefined && key["storagePath"] !== this.#config["storagePath"]) { + if (!canChangeStorage(key["storagePath"])) { + throw "无法改变存储地址" + return + } + this.#change(key["storagePath"]) + this.#config["storagePath"] = key["storagePath"] + changeKeys.push("storagePath") + isChange = true + } + for (const _ in key) { + if (Object.prototype.hasOwnProperty.call(key, _)) { + const v = key[_] + if (v != undefined && _ !== "storagePath" && v !== this.#config[_]) { + this.#config[_] = v + changeKeys.push(_ as keyof IConfig) + isChange = true + } + } + } + } + if (isChange) { + this.#sync() + this.#runCB(this.#config, oldMainConfig, changeKeys) + } + } + values<T extends keyof IConfig>(key: T): IConfig[T] { + return this.#config[key] + } +} + +const Setting = new SettingClass() + +export default Setting +export { Setting } diff --git a/packages/setting/package.json b/packages/setting/package.json new file mode 100644 index 0000000..c9dd09f --- /dev/null +++ b/packages/setting/package.json @@ -0,0 +1,7 @@ +{ + "name": "setting", + "version": "1.0.0", + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99d90c4..17effe0 100644 --- a/pnpm-lock.yaml +++ b/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: diff --git a/src/main/App.ts b/src/main/App.ts index e87ba17..6c2bcd9 100644 --- a/src/main/App.ts +++ b/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" diff --git a/src/main/index.ts b/src/main/index.ts index b61912c..0931bad 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,7 @@ import "reflect-metadata" +import "logger/main" +import "setting/main" + import { _ioc } from "main/_ioc" import { App } from "main/App" @@ -14,61 +17,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 +64,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 + } }) diff --git a/src/main/modules/_ioc.ts b/src/main/modules/_ioc.ts index c9ade3e..52cfab7 100644 --- a/src/main/modules/_ioc.ts +++ b/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(), diff --git a/src/main/modules/db/index.ts b/src/main/modules/db/index.ts index 78fd619..4cc077d 100644 --- a/src/main/modules/db/index.ts +++ b/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) } diff --git a/src/main/modules/setting/index.ts b/src/main/modules/setting/index.ts deleted file mode 100644 index 8215191..0000000 --- a/src/main/modules/setting/index.ts +++ /dev/null @@ -1,236 +0,0 @@ -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") - -type IConfig = IDefaultConfig - -type IOnFunc = (n: IConfig, c: IConfig, keys?: (keyof IConfig)[]) => void -type IT = (keyof IConfig)[] | keyof IConfig | "_" - -let storagePath = path.join(app.getPath("documents"), Config.app_title) -const storagePathDev = path.join(app.getPath("documents"), Config.app_title + "-dev") - -if (process.env.NODE_ENV === "development") { - storagePath = storagePathDev -} - -const _tempConfig = cloneDeep(Config.default_config as IConfig) -Object.keys(_tempConfig).forEach(key => { - if (typeof _tempConfig[key] === "string" && _tempConfig[key].includes("$storagePath$")) { - _tempConfig[key] = _tempConfig[key].replace(/\$storagePath\$/g, storagePath) - if (_tempConfig[key] && path.isAbsolute(_tempConfig[key])) { - _tempConfig[key] = path.normalize(_tempConfig[key]) - } - } -}) - -function isPath(str) { - // 使用正则表达式检查字符串是否以斜杠或盘符开头 - return /^(?:\/|[a-zA-Z]:\\)/.test(str) -} - -function init(config: IConfig) { - // 在配置初始化后执行 - Object.keys(config).forEach(key => { - if (config[key] && isPath(config[key]) && path.isAbsolute(config[key])) { - fs.ensureDirSync(config[key]) - } - }) - // 在配置初始化后执行 - // fs.ensureDirSync(config["snippet.storagePath"]) - // fs.ensureDirSync(config["bookmark.storagePath"]) -} - -// 判断是否是空文件夹 -function isEmptyDir(fPath: string) { - const pa = fs.readdirSync(fPath) - if (pa.length === 0) { - return true - } else { - return false - } -} - -@injectable() -class Setting extends BaseClass { - constructor() { - super() - debug(`Setting inited`) - this.init() - } - - destroy() { - // TODO - } - - #cb: [IT, IOnFunc][] = [] - - onChange(fn: IOnFunc, that?: any) - onChange(key: IT, fn: IOnFunc, that?: any) - onChange(fnOrType: IT | IOnFunc, fnOrThat: IOnFunc | any = null, that: any = null) { - if (typeof fnOrType === "function") { - this.#cb.push(["_", fnOrType.bind(fnOrThat)]) - } else { - this.#cb.push([fnOrType, fnOrThat.bind(that)]) - } - } - - #runCB(n: IConfig, c: IConfig, keys: (keyof IConfig)[]) { - for (let i = 0; i < this.#cb.length; i++) { - const temp = this.#cb[i] - const k = temp[0] - const fn = temp[1] - if (k === "_") { - fn(n, c, keys) - } - if (typeof k === "string" && keys.includes(k as keyof IConfig)) { - fn(n, c) - } - if (Array.isArray(k) && k.filter(v => keys.indexOf(v) !== -1).length) { - fn(n, c) - } - } - } - - #pathFile: string = - process.env.NODE_ENV === "development" - ? path.resolve(app.getPath("userData"), "./config_path-dev") - : path.resolve(app.getPath("userData"), "./config_path") - #config: IConfig = cloneDeep(_tempConfig) - #configPath(storagePath?: string): string { - return path.join(storagePath || this.#config.storagePath, "./config.json") - } - /** - * 读取配置文件变量同步 - * @param confingPath 配置文件路径 - */ - #syncVar(confingPath?: string) { - const configFile = this.#configPath(confingPath) - if (!fs.pathExistsSync(configFile)) { - fs.ensureFileSync(configFile) - fs.writeJSONSync(configFile, {}) - } - const config = fs.readJSONSync(configFile) as IConfig - confingPath && (config.storagePath = confingPath) - // 优先取本地的值 - for (const key in config) { - // if (Object.prototype.hasOwnProperty.call(this.#config, key)) { - // this.#config[key] = config[key] || this.#config[key] - // } - // 删除配置时本地的配置不会改变,想一下哪种方式更好 - this.#config[key] = config[key] || this.#config[key] - } - } - init() { - debug(`位置:${this.#pathFile}`) - - if (fs.pathExistsSync(this.#pathFile)) { - const confingPath = fs.readFileSync(this.#pathFile, { encoding: "utf8" }) - if (confingPath && fs.pathExistsSync(this.#configPath(confingPath))) { - this.#syncVar(confingPath) - // 防止增加了配置本地却没变的情况 - this.#sync(confingPath) - } else { - this.#syncVar(confingPath) - this.#sync(confingPath) - } - } else { - this.#syncVar() - this.#sync() - } - init.call(this, this.#config) - } - config() { - return this.#config - } - #sync(c?: string) { - const config = cloneDeep(this.#config) - delete config.storagePath - const p = this.#configPath(c) - fs.ensureFileSync(p) - fs.writeJSONSync(this.#configPath(c), config) - } - #change(p: string) { - const storagePath = this.#config.storagePath - if (fs.existsSync(storagePath) && !fs.existsSync(p)) { - fs.moveSync(storagePath, p) - } - if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) { - fs.moveSync(storagePath, p, { overwrite: true }) - } - fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" }) - } - reset(key: keyof IConfig) { - this.set(key, cloneDeep(_tempConfig[key])) - } - set(key: keyof IConfig | Partial<IConfig>, value?: any) { - const oldMainConfig = Object.assign({}, this.#config) - let isChange = false - const changeKeys: (keyof IConfig)[] = [] - const canChangeStorage = (targetPath: string) => { - if (fs.existsSync(oldMainConfig.storagePath) && fs.existsSync(targetPath) && !isEmptyDir(targetPath)) { - if (fs.existsSync(path.join(targetPath, "./config.json"))) { - return true - } - return false - } - return true - } - if (typeof key === "string") { - if (value != undefined && value !== this.#config[key]) { - if (key === "storagePath") { - if (!canChangeStorage(value)) { - throw "无法改变存储地址" - return - } - this.#change(value) - changeKeys.push("storagePath") - this.#config["storagePath"] = value - } else { - changeKeys.push(key) - this.#config[key as string] = value - } - isChange = true - } - } else { - if (key["storagePath"] !== undefined && key["storagePath"] !== this.#config["storagePath"]) { - if (!canChangeStorage(key["storagePath"])) { - throw "无法改变存储地址" - return - } - this.#change(key["storagePath"]) - this.#config["storagePath"] = key["storagePath"] - changeKeys.push("storagePath") - isChange = true - } - for (const _ in key) { - if (Object.prototype.hasOwnProperty.call(key, _)) { - const v = key[_] - if (v != undefined && _ !== "storagePath" && v !== this.#config[_]) { - this.#config[_] = v - changeKeys.push(_ as keyof IConfig) - isChange = true - } - } - } - } - if (isChange) { - this.#sync() - this.#runCB(this.#config, oldMainConfig, changeKeys) - } - } - values<T extends keyof IConfig>(key: T): IConfig[T] { - return this.#config[key] - } -} - -export default Setting -export { Setting } diff --git a/src/preload/index.ts b/src/preload/index.ts index 6e816ed..33cb5cf 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,6 +1,7 @@ import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron" import { electronAPI } from "@electron-toolkit/preload" import { call, callLong, callSync } from "./call" +import "logger/preload" 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 62ef43b..f113113 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -1,4 +1,8 @@ -<script setup lang="ts"></script> +<script setup lang="ts"> +logger.info('App.vue') +console.log(222); + +</script> <template> <div h-full flex flex-col overflow-hidden> diff --git a/src/types/logger.d.ts b/src/types/logger.d.ts new file mode 100644 index 0000000..a4e61e8 --- /dev/null +++ b/src/types/logger.d.ts @@ -0,0 +1,5 @@ +declare const logger: import("logger/preload").IRendererLogger + +interface Window { + logger: import("logger/preload").IRendererLogger +} diff --git a/tsconfig.node.json b/tsconfig.node.json index 78ada96..03e83b2 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -5,8 +5,11 @@ "src/main/**/*", "src/preload/**/*", "config/**/*", - "src/types/**/*", "packages/locales/main.ts", + "packages/setting/main.ts", + "packages/logger/main.ts", + "packages/logger/preload.ts", + "packages/logger/common.ts", "src/common/**/*.main.ts", "src/common/**/main/**/*", "src/common/**/main.ts", @@ -47,6 +50,12 @@ "locales/*": [ "packages/locales/*" ], + "setting/*": [ + "packages/setting/*" + ], + "logger/*": [ + "packages/logger/*" + ], } } } diff --git a/tsconfig.web.json b/tsconfig.web.json index 778190b..ae30650 100644 --- a/tsconfig.web.json +++ b/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,8 @@ ], "exclude": [ "packages/locales/main.ts", + "packages/setting/main.ts", + "packages/logger/main.ts", "src/common/**/main/**/*", "src/common/**/*.main.ts", "src/common/**/main.ts"