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"