diff --git a/docs/technical/how-to-use-logger-en.md b/docs/technical/how-to-use-logger-en.md
new file mode 100644
index 0000000000..5a6c9a4b17
--- /dev/null
+++ b/docs/technical/how-to-use-logger-en.md
@@ -0,0 +1,134 @@
+# How to use the LoggerService
+
+This is a developer document on how to use the logger.
+
+CherryStudio uses a unified logging service to print and record logs. **Unless there is a special reason, do not use `console.xxx` to print logs**
+
+The following are detailed instructions.
+
+## Usage in the `main` process
+
+### Importing
+
+```typescript
+import { loggerService } from '@logger'
+```
+
+### Setting module information (Required by convention)
+
+After the import statements, set it up as follows:
+
+```typescript
+const logger = loggerService.withContext('moduleName')
+```
+
+- `moduleName` is the name of the current file's module. It can be named after the filename, main class name, main function name, etc. The principle is to be clear and understandable.
+- `moduleName` will be printed in the terminal and will also be present in the file log, making it easier to filter.
+
+### Setting `CONTEXT` information (Optional)
+
+In `withContext`, you can also set other `CONTEXT` information:
+
+```typescript
+const logger = loggerService.withContext('moduleName', CONTEXT)
+```
+
+- `CONTEXT` is an object of the form `{ key: value, ... }`.
+- `CONTEXT` information will not be printed in the terminal, but it will be recorded in the file log, making it easier to filter.
+
+### Logging
+
+In your code, you can call `logger` at any time to record logs. The supported methods are: `error`, `warn`, `info`, `verbose`, `debug`, `silly`.
+For the meaning of each level, please refer to the section below.
+
+The following examples show how to use `logger.info` and `logger.error`. Other levels are used in the same way:
+```typescript
+logger.info('message', CONTEXT)
+logger.info('message %s %d', 'hello', 123, CONTEXT)
+logger.error('message', new Error('error message'), CONTEXT)
+```
+- `message` is a required string. All other options are optional.
+- `CONTEXT` as `{ key: value, ... }` is optional and will be recorded in the log file.
+- If an `Error` type is passed, the error stack will be automatically recorded.
+
+### Log Levels
+
+- In the development environment, all log levels are printed to the terminal and recorded in the file log.
+- In the production environment, the default log level is `info`. Logs are only recorded to the file and are not printed to the terminal.
+
+Changing the log level:
+- You can change the log level with `logger.setLevel('newLevel')`.
+- `logger.resetLevel()` resets it to the default level.
+- `logger.getLevel()` gets the current log level.
+
+**Note:** Changing the log level has a global effect. Please do not change it arbitrarily in your code unless you are very clear about what you are doing.
+
+## Usage in the `renderer` process
+
+Usage in the `renderer` process for *importing*, *setting module information*, and *setting context information* is **exactly the same** as in the `main` process.
+The following section focuses on the differences.
+
+### `initWindowSource`
+
+In the `renderer` process, there are different `window`s. Before starting to use the `logger`, we must set the `window` information:
+
+```typescript
+loggerService.initWindowSource('windowName')
+```
+
+As a rule, we will set this in the `window`'s `entryPoint.tsx`. This ensures that `windowName` is set before it's used.
+- An error will be thrown if `windowName` is not set, and the `logger` will not work.
+- `windowName` can only be set once; subsequent attempts to set it will have no effect.
+- `windowName` will not be printed in the `devTool`'s `console`, but it will be recorded in the `main` process terminal and the file log.
+
+### Log Levels
+
+- In the development environment, all log levels are printed to the `devTool`'s `console` by default.
+- In the production environment, the default log level is `info`, and logs are printed to the `devTool`'s `console`.
+- In both development and production environments, `warn` and `error` level logs are, by default, transmitted to the `main` process and recorded in the file log.
+ - In the development environment, the `main` process terminal will also print the logs transmitted from the renderer.
+
+#### Changing the Log Level
+
+Same as in the `main` process, you can manage the log level using `setLevel('level')`, `resetLevel()`, and `getLevel()`.
+Similarly, changing the log level is a global adjustment.
+
+#### Changing the Level Transmitted to `main`
+
+Logs from the `renderer` are sent to `main` to be managed and recorded to a file centrally (according to `main`'s file logging level). By default, only `warn` and `error` level logs are transmitted to `main`.
+
+There are two ways to change the log level for transmission to `main`:
+
+##### Global Change
+
+The following methods can be used to set, reset, and get the log level for transmission to `main`, respectively.
+
+```typescript
+logger.setLogToMainLevel('newLevel')
+logger.resetLogToMainLevel()
+logger.getLogToMainLevel()
+```
+**Note:** This method has a global effect. Please do not change it arbitrarily in your code unless you are very clear about what you are doing.
+
+
+##### Per-log Change
+
+By adding `{ logToMain: true }` at the end of the log call, you can force a single log entry to be transmitted to `main` (bypassing the global log level restriction), for example:
+
+```typescript
+logger.info('message', { logToMain: true })
+```
+
+## Log Level Usage Guidelines
+
+There are many log levels. The following are the guidelines that should be followed in CherryStudio for when to use each level:
+(Arranged from highest to lowest log level)
+
+| Log Level | Core Definition & Use Case | Example |
+| :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`error`** | **Critical error causing the program to crash or core functionality to become unusable.**
This is the highest-priority log, usually requiring immediate reporting or user notification. | - Main or renderer process crash.
- Failure to read/write critical user data files (e.g., database, configuration files), preventing the application from running.
- All unhandled exceptions. |
+| **`warn`** | **Potential issue or unexpected situation that does not affect the program's core functionality.**
The program can recover or use a fallback. | - Configuration file `settings.json` is missing; started with default settings.
- Auto-update check failed, but does not affect the use of the current version.
- A non-essential plugin failed to load. |
+| **`info`** | **Records application lifecycle events and key user actions.**
This is the default level that should be recorded in a production release to trace the user's main operational path. | - Application start, exit.
- User successfully opens/saves a file.
- Main window created/closed.
- Starting an important task (e.g., "Start video export"). |
+| **`verbose`** | **More detailed flow information than `info`, used for tracing specific features.**
Enabled when diagnosing issues with a specific feature to help understand the internal execution flow. | - Loading `Toolbar` module.
- IPC message `open-file-dialog` sent from the renderer process.
- Applying filter 'Sepia' to the image. |
+| **`debug`** | **Detailed diagnostic information used during development and debugging.**
**Must not be enabled by default in production releases**, as it may contain sensitive data and impact performance. | - Parameters for function `renderImage`: `{ width: 800, ... }`.
- Specific data content received by IPC message `save-file`.
- Details of Redux/Vuex state changes in the renderer process. |
+| **`silly`** | **The most detailed, low-level information, used only for extreme debugging.**
Rarely used in regular development; only for solving very difficult problems. | - Real-time mouse coordinates `(x: 150, y: 320)`.
- Size of each data chunk when reading a file.
- Time taken for each rendered frame. |
\ No newline at end of file
diff --git a/docs/technical/how-to-use-logger-zh.md b/docs/technical/how-to-use-logger-zh.md
new file mode 100644
index 0000000000..6a58260bef
--- /dev/null
+++ b/docs/technical/how-to-use-logger-zh.md
@@ -0,0 +1,135 @@
+# 如何使用日志 LoggerService
+
+这是关于如何使用日志的开发者文档。
+
+CherryStudio使用统一的日志服务来打印和记录日志,**若无特殊原因,请勿使用`console.xxx`来打印日志**
+
+以下是详细说明
+
+
+## 在`main`进程中使用
+
+### 引入
+
+``` typescript
+import { loggerService } from '@logger'
+```
+
+### 设置module信息(规范要求)
+
+在import头之后,设置:
+
+``` typescript
+const logger = loggerService.withContext('moduleName')
+```
+
+- `moduleName`是当前文件模块的名称,命名可以以文件名、主类名、主函数名等,原则是清晰明了
+- `moduleName`会在终端中打印出来,也会在文件日志中提现,方便筛选
+
+### 设置`CONTEXT`信息(可选)
+
+在`withContext`中,也可以设置其他`CONTEXT`信息:
+
+``` typescript
+const logger = loggerService.withContext('moduleName', CONTEXT)
+```
+
+- `CONTEXT`为`{ key: value, ... }`
+- `CONTEXT`信息不会在终端中打印出来,但是会在文件日志中记录,方便筛选
+
+### 记录日志
+
+在代码中,可以随时调用 `logger` 来记录日志,支持的方法有:`error`, `warn`, `info`, `verbose`, `debug`, `silly`
+各级别的含义,请参考下面的章节。
+
+以下以 `logger.info` 和 `logger.error` 举例如何使用,其他级别是一样的:
+``` typescript
+logger.info('message', CONTEXT)
+logger.info('message %s %d', 'hello', 123, CONTEXT)
+logger.error('message', new Error('error message'), CONTEXT)
+```
+- `message` 是必填的,`string`类型,其他选项都是可选的
+- `CONTEXT`为`{ key: value, ...}` 是可选的,会在日志文件中记录
+- 如果传递了`Error`类型,会自动记录错误堆栈
+
+### 记录级别
+
+- 开发环境下,所有级别的日志都会打印到终端,并且记录到文件日志中
+- 生产环境下,默认记录级别为`info`,日志只会记录到文件,不会打印到终端
+
+更改日志记录级别:
+- 可以通过 `logger.setLevel('newLevel')` 来更改日志记录级别
+- `logger.resetLevel()` 可以重置为默认级别
+- `logger.getLevel()` 可以获取当前记录记录级别
+
+**注意** 更改日志记录级别是全局生效的,请不要在代码中随意更改,除非你非常清楚自己在做什么
+
+## 在`renderer`进程中使用
+
+在`renderer`进程中使用,*引入方法*、*设置`module`信息*、*设置`context`信息的方法*和`main`进程中是**完全一样**的。
+下面着重讲一下不同之处。
+
+### `initWindowSource`
+
+`renderer`进程中,有不同的`window`,在开始使用`logger`之前,我们必须设置`window`信息:
+
+```typescript
+loggerService.initWindowSource('windowName')
+```
+
+原则上,我们将在`window`的`entryPoint.tsx`中进行设置,这可以保证`windowName`在开始使用前已经设置好了。
+- 未设置`windowName`会报错,`logger`将不起作用
+- `windowName`只能设置一次,重复设置将不生效
+- `windowName`不会在`devTool`的`console`中打印出来,但是会在`main`进程的终端和文件日志中记录
+
+### 记录级别
+
+- 开发环境下,默认所有级别的日志都会打印到`devTool`的`console`
+- 生产环境下,默认记录级别为`info`,日志会打印到`devTool`的`console`
+- 在开发和生产环境下,默认`warn`和`error`级别的日志,会传输给`main`进程,并记录到文件日志
+ - 开发环境下,`main`进程终端中也会打印传输过来的日志
+
+#### 更改日志记录级别
+
+和`main`进程中一样,你可以通过`setLevel('level')`、`resetLevel()`和`getLevel()`来管理日志记录级别。
+同样,该日志记录级别也是全局调整的。
+
+#### 更改传输到`main`的级别
+
+将`renderer`的日志发送到`main`,并由`main`统一管理和记录到文件(根据`main`的记录到文件的级别),默认只有`warn`和`error`级别的日志会传输到`main`
+
+有以下两种方式,可以更改传输到`main`的日志级别:
+
+##### 全局更改
+
+以下方法可以分别设置、重置和获取传输到`main`的日志级别
+
+```typescript
+logger.setLogToMainLevel('newLevel')
+logger.resetLogToMainLevel()
+logger.getLogToMainLevel()
+```
+**注意** 该方法是全局生效的,请不要在代码中随意更改,除非你非常清楚自己在做什么
+
+
+##### 单条更改
+
+在日志记录的最末尾,加上`{ logToMain: true }`,即可将本条日志传输到`main`(不受全局日志级别限制),例如:
+
+```typescript
+logger.info('message', { logToMain: true })
+```
+
+## 日志级别的使用规范
+
+日志有很多级别,什么时候应该用哪个级别,下面是在CherryStudio中应该遵循的规范:
+(按日志级别从高到低排列)
+
+| 日志级别 | 核心定义与使用场景 | 示例 |
+| :------------ | :------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`error`** | **严重错误,导致程序崩溃或核心功能无法使用。**
这是最高优的日志,通常需要立即上报或提示用户。 | - 主进程或渲染进程崩溃。
- 无法读写用户关键数据文件(如数据库、配置文件),导致应用无法运行。
- 所有未捕获的异常。` |
+| **`warn`** | **潜在问题或非预期情况,但不影响程序核心功能。**
程序可以从中恢复或使用备用方案。 | - 配置文件 `settings.json` 缺失,已使用默认配置启动。
- 自动更新检查失败,但不影响当前版本使用。
- 某个非核心插件加载失败。` |
+| **`info`** | **记录应用生命周期和关键用户行为。**
这是发布版中默认应记录的级别,用于追踪用户的主要操作路径。 | - 应用启动、退出。
- 用户成功打开/保存文件。
- 主窗口创建/关闭。
- 开始执行一项重要任务(如“开始导出视频”)。` |
+| **`verbose`** | **比 `info` 更详细的流程信息,用于追踪特定功能。**
在诊断特定功能问题时开启,帮助理解内部执行流程。 | - 正在加载 `Toolbar` 模块。
- IPC 消息 `open-file-dialog` 已从渲染进程发送。
- 正在应用滤镜 'Sepia' 到图像。` |
+| **`debug`** | **开发和调试时使用的详细诊断信息。**
**严禁在发布版中默认开启**,因为它可能包含敏感数据并影响性能。 | - 函数 `renderImage` 的入参: `{ width: 800, ... }`。
- IPC 消息 `save-file` 收到的具体数据内容。
- 渲染进程中 Redux/Vuex 的 state 变更详情。` |
+| **`silly`** | **最详尽的底层信息,仅用于极限调试。**
几乎不在常规开发中使用,仅为解决棘手问题。 | - 鼠标移动的实时坐标 `(x: 150, y: 320)`。
- 读取文件时每个数据块(chunk)的大小。
- 每一次渲染帧的耗时。 |
\ No newline at end of file
diff --git a/electron.vite.config.ts b/electron.vite.config.ts
index 7930b57f35..7c3af82973 100644
--- a/electron.vite.config.ts
+++ b/electron.vite.config.ts
@@ -18,7 +18,8 @@ export default defineConfig({
alias: {
'@main': resolve('src/main'),
'@types': resolve('src/renderer/src/types'),
- '@shared': resolve('packages/shared')
+ '@shared': resolve('packages/shared'),
+ '@logger': resolve('src/main/services/LoggerService')
}
},
build: {
@@ -68,7 +69,8 @@ export default defineConfig({
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
- '@shared': resolve('packages/shared')
+ '@shared': resolve('packages/shared'),
+ '@logger': resolve('src/renderer/src/services/LoggerService')
}
},
optimizeDeps: {
diff --git a/package.json b/package.json
index e7f6770477..9f74ab288c 100644
--- a/package.json
+++ b/package.json
@@ -163,7 +163,6 @@
"electron": "35.6.0",
"electron-builder": "26.0.15",
"electron-devtools-installer": "^3.2.0",
- "electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "6.6.4",
"electron-vite": "^3.1.0",
@@ -236,6 +235,8 @@
"vite": "6.2.6",
"vitest": "^3.1.4",
"webdav": "^5.8.0",
+ "winston": "^3.17.0",
+ "winston-daily-rotate-file": "^5.0.0",
"word-extractor": "^1.0.4",
"zipread": "^1.3.3"
},
diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts
index 057e84ca76..4c2e823f31 100644
--- a/packages/shared/IpcChannel.ts
+++ b/packages/shared/IpcChannel.ts
@@ -31,6 +31,7 @@ export enum IpcChannel {
App_GetBinaryPath = 'app:get-binary-path',
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
+ App_LogToMain = 'app:log-to-main',
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
diff --git a/packages/shared/config/types.ts b/packages/shared/config/types.ts
index 28bb4acf65..cdb5d0ecab 100644
--- a/packages/shared/config/types.ts
+++ b/packages/shared/config/types.ts
@@ -9,3 +9,12 @@ export type LoaderReturn = {
message?: string
messageSource?: 'preprocess' | 'embedding'
}
+
+export type LogSourceWithContext = {
+ process: 'main' | 'renderer'
+ window?: string // only for renderer process
+ module?: string
+ context?: Record
+}
+
+export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly'
diff --git a/src/main/index.ts b/src/main/index.ts
index 97bd10bcf7..e1e05dbf30 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -5,13 +5,13 @@ import './bootstrap'
import '@main/config'
+import { loggerService } from '@logger'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { app } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
-import Logger from 'electron-log'
-import { isDev, isWin, isLinux } from './constant'
+import { isDev, isLinux, isWin } from './constant'
import { registerIpc } from './ipc'
import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService'
@@ -26,7 +26,7 @@ import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
-Logger.initialize()
+const logger = loggerService.withContext('MainEntry')
/**
* Disable hardware acceleration if setting is enabled
@@ -68,9 +68,9 @@ app.on('web-contents-created', (_, webContents) => {
webContents.on('unresponsive', async () => {
// Interrupt execution and collect call stack from unresponsive renderer
- Logger.error('Renderer unresponsive start')
+ logger.error('Renderer unresponsive start')
const callStack = await webContents.mainFrame.collectJavaScriptCallStack()
- Logger.error('Renderer unresponsive js call stack\n', callStack)
+ logger.error('Renderer unresponsive js call stack\n', callStack)
})
})
@@ -78,12 +78,12 @@ app.on('web-contents-created', (_, webContents) => {
if (!isDev) {
// handle uncaught exception
process.on('uncaughtException', (error) => {
- Logger.error('Uncaught Exception:', error)
+ logger.error('Uncaught Exception:', error)
})
// handle unhandled rejection
process.on('unhandledRejection', (reason, promise) => {
- Logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
+ logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
})
}
@@ -181,8 +181,11 @@ if (!app.requestSingleInstanceLock()) {
try {
await mcpService.cleanup()
} catch (error) {
- Logger.error('Error cleaning up MCP service:', error)
+ logger.error('Error cleaning up MCP service:', error)
}
+
+ // finish the logger
+ logger.finish()
})
// In this file you can include the rest of your app"s specific main process
diff --git a/src/main/ipc.ts b/src/main/ipc.ts
index 37abf38dff..72ffa81a38 100644
--- a/src/main/ipc.ts
+++ b/src/main/ipc.ts
@@ -2,6 +2,7 @@ import fs from 'node:fs'
import { arch } from 'node:os'
import path from 'node:path'
+import { loggerService } from '@logger'
import { isLinux, isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { handleZoomFactor } from '@main/utils/zoom'
@@ -9,7 +10,6 @@ import { UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
-import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification'
import appService from './services/AppService'
@@ -43,6 +43,8 @@ import { decrypt, encrypt } from './utils/aes'
import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission, updateAppDataConfig } from './utils/file'
import { compress, decompress } from './utils/zip'
+const logger = loggerService.withContext('IPC')
+
const fileManager = new FileStorage()
const backupManager = new BackupManager()
const exportService = new ExportService(fileManager)
@@ -66,7 +68,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
configPath: getConfigDir(),
appDataPath: app.getPath('userData'),
resourcesPath: getResourcePath(),
- logsPath: log.transports.file.getFile().path,
+ logsPath: logger.getLogsDir(),
arch: arch(),
isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env,
installPath: path.dirname(app.getPath('exe'))
@@ -145,7 +147,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
ipcMain.handle(IpcChannel.App_SetTestPlan, async (_, isActive: boolean) => {
- log.info('set test plan', isActive)
+ logger.info('set test plan', isActive)
if (isActive !== configManager.getTestPlan()) {
appUpdater.cancelDownload()
configManager.setTestPlan(isActive)
@@ -153,7 +155,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
ipcMain.handle(IpcChannel.App_SetTestChannel, async (_, channel: UpgradeChannel) => {
- log.info('set test channel', channel)
+ logger.info('set test channel', channel)
if (channel !== configManager.getTestChannel()) {
appUpdater.cancelDownload()
configManager.setTestChannel(channel)
@@ -205,10 +207,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
)
await fileManager.clearTemp()
- await fs.writeFileSync(log.transports.file.getFile().path, '')
+ // do not clear logs for now
+ // TODO clear logs
+ // await fs.writeFileSync(log.transports.file.getFile().path, '')
return { success: true }
} catch (error: any) {
- log.error('Failed to clear cache:', error)
+ logger.error('Failed to clear cache:', error)
return { success: false, error: error.message }
}
})
@@ -216,14 +220,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// get cache size
ipcMain.handle(IpcChannel.App_GetCacheSize, async () => {
const cachePath = getCacheDir()
- log.info(`Calculating cache size for path: ${cachePath}`)
+ logger.info(`Calculating cache size for path: ${cachePath}`)
try {
const sizeInBytes = await calculateDirectorySize(cachePath)
const sizeInMB = (sizeInBytes / (1024 * 1024)).toFixed(2)
return `${sizeInMB}`
} catch (error: any) {
- log.error(`Failed to calculate cache size for ${cachePath}: ${error.message}`)
+ logger.error(`Failed to calculate cache size for ${cachePath}: ${error.message}`)
return '0'
}
})
@@ -260,7 +264,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
return filePaths[0]
} catch (error: any) {
- log.error('Failed to select app data path:', error)
+ logger.error('Failed to select app data path:', error)
return null
}
})
@@ -313,7 +317,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
return { success: true }
} catch (error: any) {
- log.error('Failed to copy user data:', error)
+ logger.error('Failed to copy user data:', error)
return { success: false, error: error.message }
}
})
@@ -322,7 +326,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.App_RelaunchApp, (_, options?: Electron.RelaunchOptions) => {
// Fix for .AppImage
if (isLinux && process.env.APPIMAGE) {
- log.info('Relaunching app with options:', process.env.APPIMAGE, options)
+ logger.info('Relaunching app with options:', process.env.APPIMAGE, options)
// On Linux, we need to use the APPIMAGE environment variable to relaunch
// https://github.com/electron-userland/electron-builder/issues/1727#issuecomment-769896927
options = options || {}
@@ -554,7 +558,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// Process DXT file using the temporary path
return await dxtService.uploadDxt(event, tempPath)
} catch (error) {
- log.error('[IPC] DXT upload error:', error)
+ logger.error('DXT upload error:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to upload DXT file'
diff --git a/src/main/knowledage/ocr/MacSysOcrProvider.ts b/src/main/knowledage/ocr/MacSysOcrProvider.ts
index df281eb60b..92e95cf716 100644
--- a/src/main/knowledage/ocr/MacSysOcrProvider.ts
+++ b/src/main/knowledage/ocr/MacSysOcrProvider.ts
@@ -1,12 +1,14 @@
+import { loggerService } from '@logger'
import { isMac } from '@main/constant'
import { FileMetadata, OcrProvider } from '@types'
-import Logger from 'electron-log'
import * as fs from 'fs'
import * as path from 'path'
import { TextItem } from 'pdfjs-dist/types/src/display/api'
import BaseOcrProvider from './BaseOcrProvider'
+const logger = loggerService.withContext('MacSysOcrProvider')
+
export default class MacSysOcrProvider extends BaseOcrProvider {
private readonly MIN_TEXT_LENGTH = 1000
private MacOCR: any
@@ -21,7 +23,7 @@ export default class MacSysOcrProvider extends BaseOcrProvider {
const module = await import('@cherrystudio/mac-system-ocr')
this.MacOCR = module.default
} catch (error) {
- Logger.error('[OCR] Failed to load mac-system-ocr:', error)
+ logger.error('Failed to load mac-system-ocr:', error)
throw error
}
}
@@ -83,7 +85,7 @@ export default class MacSysOcrProvider extends BaseOcrProvider {
}
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
- Logger.info(`[OCR] Starting OCR process for file: ${file.name}`)
+ logger.info(`Starting OCR process for file: ${file.name}`)
if (file.ext === '.pdf') {
try {
const { pdf } = await import('@cherrystudio/pdf-to-img-napi')
@@ -103,7 +105,7 @@ export default class MacSysOcrProvider extends BaseOcrProvider {
await new Promise((resolve, reject) => {
writeStream.end(() => {
- Logger.info(`[OCR] OCR process completed successfully for ${file.origin_name}`)
+ logger.info(`OCR process completed successfully for ${file.origin_name}`)
resolve()
})
writeStream.on('error', reject)
@@ -119,7 +121,7 @@ export default class MacSysOcrProvider extends BaseOcrProvider {
}
}
} catch (error) {
- Logger.error('[OCR] Error during OCR process:', error)
+ logger.error('Error during OCR process:', error)
throw error
}
}
diff --git a/src/main/knowledage/ocr/OcrProviderFactory.ts b/src/main/knowledage/ocr/OcrProviderFactory.ts
index 96d95a63ad..34b8fe6f1d 100644
--- a/src/main/knowledage/ocr/OcrProviderFactory.ts
+++ b/src/main/knowledage/ocr/OcrProviderFactory.ts
@@ -1,16 +1,19 @@
+import { loggerService } from '@logger'
import { isMac } from '@main/constant'
import { OcrProvider } from '@types'
-import Logger from 'electron-log'
import BaseOcrProvider from './BaseOcrProvider'
import DefaultOcrProvider from './DefaultOcrProvider'
import MacSysOcrProvider from './MacSysOcrProvider'
+
+const logger = loggerService.withContext('OcrProviderFactory')
+
export default class OcrProviderFactory {
static create(provider: OcrProvider): BaseOcrProvider {
switch (provider.id) {
case 'system':
if (!isMac) {
- Logger.warn('[OCR] System OCR provider is only available on macOS')
+ logger.warn('System OCR provider is only available on macOS')
}
return new MacSysOcrProvider(provider)
default:
diff --git a/src/main/knowledage/preprocess/Doc2xPreprocessProvider.ts b/src/main/knowledage/preprocess/Doc2xPreprocessProvider.ts
index 5851d33b86..f34b518c22 100644
--- a/src/main/knowledage/preprocess/Doc2xPreprocessProvider.ts
+++ b/src/main/knowledage/preprocess/Doc2xPreprocessProvider.ts
@@ -1,13 +1,15 @@
import fs from 'node:fs'
import path from 'node:path'
+import { loggerService } from '@logger'
import { FileMetadata, PreprocessProvider } from '@types'
import AdmZip from 'adm-zip'
import axios, { AxiosRequestConfig } from 'axios'
-import Logger from 'electron-log'
import BasePreprocessProvider from './BasePreprocessProvider'
+const logger = loggerService.withContext('Doc2xPreprocessProvider')
+
type ApiResponse = {
code: string
data: T
@@ -52,11 +54,11 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
try {
- Logger.info(`Preprocess processing started: ${file.path}`)
+ logger.info(`Preprocess processing started: ${file.path}`)
// 步骤1: 准备上传
const { uid, url } = await this.preupload()
- Logger.info(`Preprocess preupload completed: uid=${uid}`)
+ logger.info(`Preprocess preupload completed: uid=${uid}`)
await this.validateFile(file.path)
@@ -65,7 +67,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
// 步骤3: 等待处理完成
await this.waitForProcessing(sourceId, uid)
- Logger.info(`Preprocess parsing completed successfully for: ${file.path}`)
+ logger.info(`Preprocess parsing completed successfully for: ${file.path}`)
// 步骤4: 导出文件
const { path: outputPath } = await this.exportFile(file, uid)
@@ -75,7 +77,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
processedFile: this.createProcessedFileInfo(file, outputPath)
}
} catch (error) {
- Logger.error(
+ logger.error(
`Preprocess processing failed for ${file.path}: ${error instanceof Error ? error.message : String(error)}`
)
throw error
@@ -100,11 +102,11 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
* @returns 导出文件的路径
*/
public async exportFile(file: FileMetadata, uid: string): Promise<{ path: string }> {
- Logger.info(`Exporting file: ${file.path}`)
+ logger.info(`Exporting file: ${file.path}`)
// 步骤1: 转换文件
await this.convertFile(uid, file.path)
- Logger.info(`File conversion completed for: ${file.path}`)
+ logger.info(`File conversion completed for: ${file.path}`)
// 步骤2: 等待导出并获取URL
const exportUrl = await this.waitForExport(uid)
@@ -123,7 +125,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
await this.delay(1000)
const { status, progress } = await this.getStatus(uid)
await this.sendPreprocessProgress(sourceId, progress)
- Logger.info(`Preprocess processing status: ${status}, progress: ${progress}%`)
+ logger.info(`Preprocess processing status: ${status}, progress: ${progress}%`)
if (status === 'success') {
return
@@ -142,7 +144,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
while (true) {
await this.delay(1000)
const { status, url } = await this.getParsedFile(uid)
- Logger.info(`Export status: ${status}`)
+ logger.info(`Export status: ${status}`)
if (status === 'success' && url) {
return url
@@ -169,7 +171,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
throw new Error(`API returned error: ${data.message || JSON.stringify(data)}`)
}
} catch (error) {
- Logger.error(`Failed to get preupload URL: ${error instanceof Error ? error.message : String(error)}`)
+ logger.error(`Failed to get preupload URL: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to get preupload URL')
}
}
@@ -188,7 +190,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
throw new Error(`HTTP status ${response.status}: ${response.statusText}`)
}
} catch (error) {
- Logger.error(`Failed to upload file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
+ logger.error(`Failed to upload file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to upload file')
}
}
@@ -206,7 +208,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
throw new Error(`API returned error: ${response.data.message || JSON.stringify(response.data)}`)
}
} catch (error) {
- Logger.error(`Failed to get status for uid ${uid}: ${error instanceof Error ? error.message : String(error)}`)
+ logger.error(`Failed to get status for uid ${uid}: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to get processing status')
}
}
@@ -242,7 +244,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
throw new Error(`API returned error: ${response.data.message || JSON.stringify(response.data)}`)
}
} catch (error) {
- Logger.error(`Failed to convert file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
+ logger.error(`Failed to convert file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to convert file')
}
}
@@ -265,7 +267,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
throw new Error(`HTTP status ${response.status}: ${response.statusText}`)
}
} catch (error) {
- Logger.error(
+ logger.error(
`Failed to get parsed file for uid ${uid}: ${error instanceof Error ? error.message : String(error)}`
)
throw new Error('Failed to get parsed file information')
@@ -288,7 +290,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
fs.mkdirSync(dirPath, { recursive: true })
fs.mkdirSync(extractPath, { recursive: true })
- Logger.info(`Downloading to export path: ${zipPath}`)
+ logger.info(`Downloading to export path: ${zipPath}`)
try {
// 下载文件
@@ -303,14 +305,14 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
// 解压文件
const zip = new AdmZip(zipPath)
zip.extractAllTo(extractPath, true)
- Logger.info(`Extracted files to: ${extractPath}`)
+ logger.info(`Extracted files to: ${extractPath}`)
// 删除临时ZIP文件
fs.unlinkSync(zipPath)
return { path: extractPath }
} catch (error) {
- Logger.error(`Failed to download and extract file: ${error instanceof Error ? error.message : String(error)}`)
+ logger.error(`Failed to download and extract file: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to download and extract file')
}
}
diff --git a/src/main/knowledage/preprocess/MineruPreprocessProvider.ts b/src/main/knowledage/preprocess/MineruPreprocessProvider.ts
index 58c0c00c23..1362ffa521 100644
--- a/src/main/knowledage/preprocess/MineruPreprocessProvider.ts
+++ b/src/main/knowledage/preprocess/MineruPreprocessProvider.ts
@@ -1,13 +1,15 @@
import fs from 'node:fs'
import path from 'node:path'
+import { loggerService } from '@logger'
import { FileMetadata, PreprocessProvider } from '@types'
import AdmZip from 'adm-zip'
import axios from 'axios'
-import Logger from 'electron-log'
import BasePreprocessProvider from './BasePreprocessProvider'
+const logger = loggerService.withContext('MineruPreprocessProvider')
+
type ApiResponse = {
code: number
data: T
@@ -61,16 +63,16 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
file: FileMetadata
): Promise<{ processedFile: FileMetadata; quota: number }> {
try {
- Logger.info(`MinerU preprocess processing started: ${file.path}`)
+ logger.info(`MinerU preprocess processing started: ${file.path}`)
await this.validateFile(file.path)
// 1. 获取上传URL并上传文件
const batchId = await this.uploadFile(file)
- Logger.info(`MinerU file upload completed: batch_id=${batchId}`)
+ logger.info(`MinerU file upload completed: batch_id=${batchId}`)
// 2. 等待处理完成并获取结果
const extractResult = await this.waitForCompletion(sourceId, batchId, file.origin_name)
- Logger.info(`MinerU processing completed for batch: ${batchId}`)
+ logger.info(`MinerU processing completed for batch: ${batchId}`)
// 3. 下载并解压文件
const { path: outputPath } = await this.downloadAndExtractFile(extractResult.full_zip_url!, file)
@@ -84,7 +86,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
quota
}
} catch (error: any) {
- Logger.error(`MinerU preprocess processing failed for ${file.path}: ${error.message}`)
+ logger.error(`MinerU preprocess processing failed for ${file.path}: ${error.message}`)
throw new Error(error.message)
}
}
@@ -105,7 +107,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
const response: QuotaResponse = await quota.json()
return response.data.user_left_quota
} catch (error) {
- console.error('Error checking quota:', error)
+ logger.error('Error checking quota:', error)
throw error
}
}
@@ -143,16 +145,16 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
try {
fs.renameSync(originalMdPath, newMdPath)
finalPath = newMdPath
- Logger.info(`Renamed markdown file from ${mdFile} to ${finalName}`)
+ logger.info(`Renamed markdown file from ${mdFile} to ${finalName}`)
} catch (renameError) {
- Logger.warn(`Failed to rename file ${mdFile} to ${finalName}: ${renameError}`)
+ logger.warn(`Failed to rename file ${mdFile} to ${finalName}: ${renameError}`)
// 如果重命名失败,使用原文件
finalPath = originalMdPath
finalName = mdFile
}
}
} catch (error) {
- Logger.warn(`Failed to read output directory ${outputPath}: ${error}`)
+ logger.warn(`Failed to read output directory ${outputPath}: ${error}`)
finalPath = path.join(outputPath, `${file.id}.md`)
}
@@ -171,13 +173,13 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
const zipPath = path.join(dirPath, `${file.id}.zip`)
const extractPath = path.join(dirPath, `${file.id}`)
- Logger.info(`Downloading MinerU result to: ${zipPath}`)
+ logger.info(`Downloading MinerU result to: ${zipPath}`)
try {
// 下载ZIP文件
const response = await axios.get(zipUrl, { responseType: 'arraybuffer' })
fs.writeFileSync(zipPath, response.data)
- Logger.info(`Downloaded ZIP file: ${zipPath}`)
+ logger.info(`Downloaded ZIP file: ${zipPath}`)
// 确保提取目录存在
if (!fs.existsSync(extractPath)) {
@@ -187,14 +189,14 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
// 解压文件
const zip = new AdmZip(zipPath)
zip.extractAllTo(extractPath, true)
- Logger.info(`Extracted files to: ${extractPath}`)
+ logger.info(`Extracted files to: ${extractPath}`)
// 删除临时ZIP文件
fs.unlinkSync(zipPath)
return { path: extractPath }
} catch (error: any) {
- Logger.error(`Failed to download and extract file: ${error.message}`)
+ logger.error(`Failed to download and extract file: ${error.message}`)
throw new Error(error.message)
}
}
@@ -203,16 +205,16 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
try {
// 步骤1: 获取上传URL
const { batchId, fileUrls } = await this.getBatchUploadUrls(file)
- Logger.info(`Got upload URLs for batch: ${batchId}`)
+ logger.info(`Got upload URLs for batch: ${batchId}`)
console.log('batchId:', batchId, 'fileurls:', fileUrls)
// 步骤2: 上传文件到获取的URL
await this.putFileToUrl(file.path, fileUrls[0])
- Logger.info(`File uploaded successfully: ${file.path}`)
+ logger.info(`File uploaded successfully: ${file.path}`)
return batchId
} catch (error: any) {
- Logger.error(`Failed to upload file ${file.path}: ${error.message}`)
+ logger.error(`Failed to upload file ${file.path}: ${error.message}`)
throw new Error(error.message)
}
}
@@ -260,7 +262,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
} catch (error: any) {
- Logger.error(`Failed to get batch upload URLs: ${error.message}`)
+ logger.error(`Failed to get batch upload URLs: ${error.message}`)
throw new Error(error.message)
}
}
@@ -296,16 +298,16 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
body: responseBody
}
- console.error('Response details:', errorInfo)
+ logger.error('Response details:', errorInfo)
throw new Error(`Upload failed with status ${response.status}: ${responseBody}`)
} catch (parseError) {
throw new Error(`Upload failed with status ${response.status}. Could not parse response body.`)
}
}
- Logger.info(`File uploaded successfully to: ${uploadUrl}`)
+ logger.info(`File uploaded successfully to: ${uploadUrl}`)
} catch (error: any) {
- Logger.error(`Failed to upload file to URL ${uploadUrl}: ${error}`)
+ logger.error(`Failed to upload file to URL ${uploadUrl}: ${error}`)
throw new Error(error.message)
}
}
@@ -334,7 +336,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
} catch (error: any) {
- Logger.error(`Failed to get extract results for batch ${batchId}: ${error.message}`)
+ logger.error(`Failed to get extract results for batch ${batchId}: ${error.message}`)
throw new Error(error.message)
}
}
@@ -360,7 +362,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
// 检查处理状态
if (fileResult.state === 'done' && fileResult.full_zip_url) {
- Logger.info(`Processing completed for file: ${fileName}`)
+ logger.info(`Processing completed for file: ${fileName}`)
return fileResult
} else if (fileResult.state === 'failed') {
throw new Error(`Processing failed for file: ${fileName}, error: ${fileResult.err_msg}`)
@@ -371,15 +373,15 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
(fileResult.extract_progress.extracted_pages / fileResult.extract_progress.total_pages) * 100
)
await this.sendPreprocessProgress(sourceId, progress)
- Logger.info(`File ${fileName} processing progress: ${progress}%`)
+ logger.info(`File ${fileName} processing progress: ${progress}%`)
} else {
// 如果没有具体进度信息,发送一个通用进度
await this.sendPreprocessProgress(sourceId, 50)
- Logger.info(`File ${fileName} is still processing...`)
+ logger.info(`File ${fileName} is still processing...`)
}
}
} catch (error) {
- Logger.warn(`Failed to check status for batch ${batchId}, retry ${retries + 1}/${maxRetries}`)
+ logger.warn(`Failed to check status for batch ${batchId}, retry ${retries + 1}/${maxRetries}`)
if (retries === maxRetries - 1) {
throw error
}
diff --git a/src/main/knowledage/preprocess/MistralPreprocessProvider.ts b/src/main/knowledage/preprocess/MistralPreprocessProvider.ts
index 3150162801..ece371ddb7 100644
--- a/src/main/knowledage/preprocess/MistralPreprocessProvider.ts
+++ b/src/main/knowledage/preprocess/MistralPreprocessProvider.ts
@@ -1,5 +1,6 @@
import fs from 'node:fs'
+import { loggerService } from '@logger'
import { MistralClientManager } from '@main/services/MistralClientManager'
import { MistralService } from '@main/services/remotefile/MistralService'
import { Mistral } from '@mistralai/mistralai'
@@ -7,13 +8,14 @@ import { DocumentURLChunk } from '@mistralai/mistralai/models/components/documen
import { ImageURLChunk } from '@mistralai/mistralai/models/components/imageurlchunk'
import { OCRResponse } from '@mistralai/mistralai/models/components/ocrresponse'
import { FileMetadata, FileTypes, PreprocessProvider, Provider } from '@types'
-import Logger from 'electron-log'
import path from 'path'
import BasePreprocessProvider from './BasePreprocessProvider'
type PreuploadResponse = DocumentURLChunk | ImageURLChunk
+const logger = loggerService.withContext('MistralPreprocessProvider')
+
export default class MistralPreprocessProvider extends BasePreprocessProvider {
private sdk: Mistral
private fileService: MistralService
@@ -36,20 +38,20 @@ export default class MistralPreprocessProvider extends BasePreprocessProvider {
private async preupload(file: FileMetadata): Promise {
let document: PreuploadResponse
- Logger.info(`preprocess preupload started for local file: ${file.path}`)
+ logger.info(`preprocess preupload started for local file: ${file.path}`)
if (file.ext.toLowerCase() === '.pdf') {
const uploadResponse = await this.fileService.uploadFile(file)
if (uploadResponse.status === 'failed') {
- Logger.error('File upload failed:', uploadResponse)
+ logger.error('File upload failed:', uploadResponse)
throw new Error('Failed to upload file: ' + uploadResponse.displayName)
}
await this.sendPreprocessProgress(file.id, 15)
const fileUrl = await this.sdk.files.getSignedUrl({
fileId: uploadResponse.fileId
})
- Logger.info('Got signed URL:', fileUrl)
+ logger.info('Got signed URL:', fileUrl)
await this.sendPreprocessProgress(file.id, 20)
document = {
type: 'document_url',
@@ -152,7 +154,7 @@ export default class MistralPreprocessProvider extends BasePreprocessProvider {
counter++
} catch (error) {
- Logger.error(`Failed to save image ${imageFileName}:`, error)
+ logger.error(`Failed to save image ${imageFileName}:`, error)
}
}
})
diff --git a/src/main/knowledge/embeddings/VoyageEmbeddings.ts b/src/main/knowledge/embeddings/VoyageEmbeddings.ts
index 61f23ad767..a8260a4ae9 100644
--- a/src/main/knowledge/embeddings/VoyageEmbeddings.ts
+++ b/src/main/knowledge/embeddings/VoyageEmbeddings.ts
@@ -1,8 +1,11 @@
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
+import { loggerService } from '@logger'
import { VOYAGE_SUPPORTED_DIM_MODELS } from './utils'
+const logger = loggerService.withContext('VoyageEmbeddings')
+
/**
* 支持设置嵌入维度的模型
*/
@@ -16,7 +19,7 @@ export class VoyageEmbeddings extends BaseEmbeddings {
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
if (!VOYAGE_SUPPORTED_DIM_MODELS.includes(this.configuration.modelName) && this.configuration.outputDimension) {
- console.error(`VoyageEmbeddings only supports ${VOYAGE_SUPPORTED_DIM_MODELS.join(', ')} to set outputDimension.`)
+ logger.error(`VoyageEmbeddings only supports ${VOYAGE_SUPPORTED_DIM_MODELS.join(', ')} to set outputDimension.`)
this.model = new _VoyageEmbeddings({ ...this.configuration, outputDimension: undefined })
} else {
this.model = new _VoyageEmbeddings(this.configuration)
diff --git a/src/main/knowledge/loader/epubLoader.ts b/src/main/knowledge/loader/epubLoader.ts
index bb62cba8cb..bd90a9c7fb 100644
--- a/src/main/knowledge/loader/epubLoader.ts
+++ b/src/main/knowledge/loader/epubLoader.ts
@@ -1,12 +1,14 @@
import { BaseLoader } from '@cherrystudio/embedjs-interfaces'
import { cleanString } from '@cherrystudio/embedjs-utils'
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
+import { loggerService } from '@logger'
import { getTempDir } from '@main/utils/file'
-import Logger from 'electron-log'
import EPub from 'epub'
import * as fs from 'fs'
import path from 'path'
+const logger = loggerService.withContext('EpubLoader')
+
/**
* epub 加载器的配置选项
*/
@@ -183,7 +185,7 @@ export class EpubLoader extends BaseLoader = {
// 内置类型
@@ -75,7 +77,7 @@ export async function addFileLoader(
// JSON类型处理
let jsonObject = {}
let jsonParsed = true
- Logger.info(`[KnowledgeBase] processing file ${file.path} as ${loaderType} type`)
+ logger.info(`[KnowledgeBase] processing file ${file.path} as ${loaderType} type`)
switch (loaderType) {
case 'common':
// 内置类型处理
@@ -127,7 +129,7 @@ export async function addFileLoader(
jsonObject = JSON.parse(await readTextFileWithAutoEncoding(file.path))
} catch (error) {
jsonParsed = false
- Logger.warn('[KnowledgeBase] failed parsing json file, falling back to text processing:', file.path, error)
+ logger.warn('[KnowledgeBase] failed parsing json file, falling back to text processing:', file.path, error)
}
if (jsonParsed) {
diff --git a/src/main/knowledge/loader/odLoader.ts b/src/main/knowledge/loader/odLoader.ts
index 7e13420b24..f2893d3982 100644
--- a/src/main/knowledge/loader/odLoader.ts
+++ b/src/main/knowledge/loader/odLoader.ts
@@ -1,9 +1,12 @@
import { BaseLoader } from '@cherrystudio/embedjs-interfaces'
import { cleanString } from '@cherrystudio/embedjs-utils'
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
+import { loggerService } from '@logger'
import md5 from 'md5'
import { OfficeParserConfig, parseOfficeAsync } from 'officeparser'
+const logger = loggerService.withContext('OdLoader')
+
export enum OdType {
OdtLoader = 'OdtLoader',
OdsLoader = 'OdsLoader',
@@ -42,7 +45,7 @@ export class OdLoader extends BaseLoader<{ type: string }> {
try {
this.extractedText = await parseOfficeAsync(this.filePath, this.config)
} catch (err) {
- console.error('odLoader error', err)
+ logger.error('odLoader error', err)
throw err
}
}
diff --git a/src/main/mcpServers/dify-knowledge.ts b/src/main/mcpServers/dify-knowledge.ts
index 33c61820c0..db62f6cddd 100644
--- a/src/main/mcpServers/dify-knowledge.ts
+++ b/src/main/mcpServers/dify-knowledge.ts
@@ -1,9 +1,12 @@
// inspired by https://dify.ai/blog/turn-your-dify-app-into-an-mcp-server
+import { loggerService } from '@logger'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
+const logger = loggerService.withContext('DifyKnowledgeServer')
+
interface DifyKnowledgeServerConfig {
difyKey: string
apiHost: string
@@ -168,7 +171,7 @@ class DifyKnowledgeServer {
content: [{ type: 'text', text: formattedText }]
}
} catch (error) {
- console.error('获取知识库列表时出错:', error)
+ logger.error('Error fetching knowledge list:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
// 返回包含错误信息的 MCP 响应
return {
@@ -247,7 +250,7 @@ class DifyKnowledgeServer {
content: [{ type: 'text', text: formattedText }]
}
} catch (error) {
- console.error('搜索知识库时出错:', error)
+ logger.error('Error searching knowledge:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
return {
content: [{ type: 'text', text: `Search Knowledge Error: ${errorMessage}` }],
diff --git a/src/main/mcpServers/factory.ts b/src/main/mcpServers/factory.ts
index 2376ef223e..fe1269cec2 100644
--- a/src/main/mcpServers/factory.ts
+++ b/src/main/mcpServers/factory.ts
@@ -1,5 +1,5 @@
+import { loggerService } from '@logger'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
-import Logger from 'electron-log'
import BraveSearchServer from './brave-search'
import DifyKnowledgeServer from './dify-knowledge'
@@ -9,8 +9,10 @@ import MemoryServer from './memory'
import PythonServer from './python'
import ThinkingServer from './sequentialthinking'
+const logger = loggerService.withContext('MCPFactory')
+
export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record = {}): Server {
- Logger.info(`[MCP] Creating in-memory MCP server: ${name} with args: ${args} and envs: ${JSON.stringify(envs)}`)
+ logger.debug(`[MCP] Creating in-memory MCP server: ${name} with args: ${args} and envs: ${JSON.stringify(envs)}`)
switch (name) {
case '@cherry/memory': {
const envPath = envs.MEMORY_FILE_PATH
diff --git a/src/main/mcpServers/filesystem.ts b/src/main/mcpServers/filesystem.ts
index 4d99507ba2..ff72973c29 100644
--- a/src/main/mcpServers/filesystem.ts
+++ b/src/main/mcpServers/filesystem.ts
@@ -1,5 +1,6 @@
// port https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts
+import { loggerService } from '@logger'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { createTwoFilesPatch } from 'diff'
@@ -10,6 +11,8 @@ import path from 'path'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
+const logger = loggerService.withContext('MCP:FileSystemServer')
+
// Normalize all paths consistently
function normalizePath(p: string): string {
return path.normalize(p)
@@ -294,7 +297,7 @@ class FileSystemServer {
// Validate that all directories exist and are accessible
this.validateDirs().catch((error) => {
- console.error('Error validating allowed directories:', error)
+ logger.error('Error validating allowed directories:', error)
throw new Error(`Error validating allowed directories: ${error}`)
})
@@ -319,11 +322,11 @@ class FileSystemServer {
try {
const stats = await fs.stat(expandHome(dir))
if (!stats.isDirectory()) {
- console.error(`Error: ${dir} is not a directory`)
+ logger.error(`Error: ${dir} is not a directory`)
throw new Error(`Error: ${dir} is not a directory`)
}
} catch (error: any) {
- console.error(`Error accessing directory ${dir}:`, error)
+ logger.error(`Error accessing directory ${dir}:`, error)
throw new Error(`Error accessing directory ${dir}:`, error)
}
})
diff --git a/src/main/mcpServers/memory.ts b/src/main/mcpServers/memory.ts
index 746670b36e..912999b6d8 100644
--- a/src/main/mcpServers/memory.ts
+++ b/src/main/mcpServers/memory.ts
@@ -1,11 +1,13 @@
+import { loggerService } from '@logger'
import { getConfigDir } from '@main/utils/file'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
import { Mutex } from 'async-mutex' // 引入 Mutex
-import Logger from 'electron-log'
import { promises as fs } from 'fs'
import path from 'path'
+const logger = loggerService.withContext('MCPServer:Memory')
+
// Define memory file path
const defaultMemoryPath = path.join(getConfigDir(), 'memory.json')
@@ -61,7 +63,7 @@ class KnowledgeGraphManager {
await fs.writeFile(this.memoryPath, JSON.stringify({ entities: [], relations: [] }, null, 2))
}
} catch (error) {
- console.error('Failed to ensure memory path exists:', error)
+ logger.error('Failed to ensure memory path exists:', error)
// Propagate the error or handle it more gracefully depending on requirements
throw new McpError(
ErrorCode.InternalError,
@@ -94,13 +96,13 @@ class KnowledgeGraphManager {
this.relations = new Set()
await this._persistGraph() // Create the file with empty structure
} else if (error instanceof SyntaxError) {
- console.error('Failed to parse memory.json, initializing with empty graph:', error)
+ logger.error('Failed to parse memory.json, initializing with empty graph:', error)
// If JSON is invalid, start fresh and overwrite the corrupted file
this.entities = new Map()
this.relations = new Set()
await this._persistGraph()
} else {
- console.error('Failed to load knowledge graph from disk:', error)
+ logger.error('Failed to load knowledge graph from disk:', error)
throw new McpError(
ErrorCode.InternalError,
`Failed to load graph: ${error instanceof Error ? error.message : String(error)}`
@@ -119,7 +121,7 @@ class KnowledgeGraphManager {
}
await fs.writeFile(this.memoryPath, JSON.stringify(graphData, null, 2))
} catch (error) {
- console.error('Failed to save knowledge graph:', error)
+ logger.error('Failed to save knowledge graph:', error)
// Decide how to handle write errors - potentially retry or notify
throw new McpError(
ErrorCode.InternalError,
@@ -162,7 +164,7 @@ class KnowledgeGraphManager {
relations.forEach((relation) => {
// Ensure related entities exist before creating a relation
if (!this.entities.has(relation.from) || !this.entities.has(relation.to)) {
- console.warn(`Skipping relation creation: Entity not found for relation ${relation.from} -> ${relation.to}`)
+ logger.warn(`Skipping relation creation: Entity not found for relation ${relation.from} -> ${relation.to}`)
return // Skip this relation
}
const relationStr = this._serializeRelation(relation)
@@ -188,7 +190,7 @@ class KnowledgeGraphManager {
// Option 1: Throw error
throw new McpError(ErrorCode.InvalidParams, `Entity with name ${o.entityName} not found`)
// Option 2: Skip and warn
- // console.warn(`Entity with name ${o.entityName} not found when adding observations. Skipping.`);
+ // logger.warn(`Entity with name ${o.entityName} not found when adding observations. Skipping.`);
// return;
}
// Ensure observations array exists
@@ -356,9 +358,9 @@ class MemoryServer {
private async _initializeManager(memoryPath: string): Promise {
try {
this.knowledgeGraphManager = await KnowledgeGraphManager.create(memoryPath)
- Logger.log('KnowledgeGraphManager initialized successfully.')
+ logger.debug('KnowledgeGraphManager initialized successfully.')
} catch (error) {
- Logger.error('Failed to initialize KnowledgeGraphManager:', error)
+ logger.error('Failed to initialize KnowledgeGraphManager:', error)
// Server might be unusable, consider how to handle this state
// Maybe set a flag and return errors for all tool calls?
this.knowledgeGraphManager = null // Ensure it's null if init fails
@@ -385,7 +387,7 @@ class MemoryServer {
await this._getManager() // Wait for initialization before confirming tools are available
} catch (error) {
// If manager failed to init, maybe return an empty tool list or throw?
- console.error('Cannot list tools, manager initialization failed:', error)
+ logger.error('Cannot list tools, manager initialization failed:', error)
return { tools: [] } // Return empty list if server is not ready
}
@@ -687,7 +689,7 @@ class MemoryServer {
if (error instanceof McpError) {
throw error // Re-throw McpErrors directly
}
- console.error(`Error executing tool ${name}:`, error)
+ logger.error(`Error executing tool ${name}:`, error)
// Throw a generic internal error for unexpected issues
throw new McpError(
ErrorCode.InternalError,
diff --git a/src/main/mcpServers/python.ts b/src/main/mcpServers/python.ts
index 6fe0b80db1..b8a4208088 100644
--- a/src/main/mcpServers/python.ts
+++ b/src/main/mcpServers/python.ts
@@ -1,7 +1,9 @@
+import { loggerService } from '@logger'
import { pythonService } from '@main/services/PythonService'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
-import Logger from 'electron-log'
+
+const logger = loggerService.withContext('MCPServer:Python')
/**
* Python MCP Server for executing Python code using Pyodide
@@ -88,7 +90,7 @@ print('python code here')`,
throw new McpError(ErrorCode.InvalidParams, 'Code parameter is required and must be a string')
}
- Logger.info('Executing Python code via Pyodide')
+ logger.debug('Executing Python code via Pyodide')
const result = await pythonService.executeScript(code, context, timeout)
@@ -102,7 +104,7 @@ print('python code here')`,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
- Logger.error('Python execution error:', errorMessage)
+ logger.error('Python execution error:', errorMessage)
throw new McpError(ErrorCode.InternalError, `Python execution failed: ${errorMessage}`)
}
diff --git a/src/main/mcpServers/sequentialthinking.ts b/src/main/mcpServers/sequentialthinking.ts
index bcda96e192..90c1c329d5 100644
--- a/src/main/mcpServers/sequentialthinking.ts
+++ b/src/main/mcpServers/sequentialthinking.ts
@@ -1,11 +1,14 @@
// Sequential Thinking MCP Server
// port https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/index.ts
+import { loggerService } from '@logger'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
// Fixed chalk import for ESM
import chalk from 'chalk'
+const logger = loggerService.withContext('MCPServer:SequentialThinkingServer')
+
interface ThoughtData {
thought: string
thoughtNumber: number
@@ -98,7 +101,7 @@ class SequentialThinkingServer {
}
const formattedThought = this.formatThought(validatedInput)
- console.error(formattedThought)
+ logger.error(formattedThought)
return {
content: [
diff --git a/src/main/services/AppService.ts b/src/main/services/AppService.ts
index f7dc5a7657..b95edda562 100644
--- a/src/main/services/AppService.ts
+++ b/src/main/services/AppService.ts
@@ -1,10 +1,12 @@
+import { loggerService } from '@logger'
import { isDev, isLinux, isMac, isWin } from '@main/constant'
import { app } from 'electron'
-import log from 'electron-log'
import fs from 'fs'
import os from 'os'
import path from 'path'
+const logger = loggerService.withContext('AppService')
+
export class AppService {
private static instance: AppService
@@ -59,19 +61,19 @@ export class AppService {
// Write desktop file
await fs.promises.writeFile(desktopFile, desktopContent)
- log.info('Created autostart desktop file for Linux')
+ logger.info('Created autostart desktop file for Linux')
} else {
// Remove desktop file
try {
await fs.promises.access(desktopFile)
await fs.promises.unlink(desktopFile)
- log.info('Removed autostart desktop file for Linux')
+ logger.info('Removed autostart desktop file for Linux')
} catch {
// File doesn't exist, no need to remove
}
}
} catch (error) {
- log.error('Failed to set launch on boot for Linux:', error)
+ logger.error('Failed to set launch on boot for Linux:', error)
}
}
}
diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts
index effcff00c5..ed67e5319c 100644
--- a/src/main/services/AppUpdater.ts
+++ b/src/main/services/AppUpdater.ts
@@ -1,3 +1,4 @@
+import { loggerService } from '@logger'
import { isWin } from '@main/constant'
import { locales } from '@main/utils/locales'
import { generateUserAgent } from '@main/utils/systemInfo'
@@ -5,13 +6,14 @@ import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron'
-import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater, NsisUpdater, UpdateCheckResult } from 'electron-updater'
import path from 'path'
import icon from '../../../build/icon.png?asset'
import { configManager } from './ConfigManager'
+const logger = loggerService.withContext('AppUpdater')
+
export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater
private releaseInfo: UpdateInfo | undefined
@@ -19,8 +21,6 @@ export default class AppUpdater {
private updateCheckResult: UpdateCheckResult | null = null
constructor(mainWindow: BrowserWindow) {
- logger.transports.file.level = 'info'
-
autoUpdater.logger = logger
autoUpdater.forceDevUpdateConfig = !app.isPackaged
autoUpdater.autoDownload = configManager.getAutoUpdate()
diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts
index 2b607cb51a..5e0f12d844 100644
--- a/src/main/services/BackupManager.ts
+++ b/src/main/services/BackupManager.ts
@@ -1,10 +1,10 @@
+import { loggerService } from '@logger'
import { IpcChannel } from '@shared/IpcChannel'
import { WebDavConfig } from '@types'
import { S3Config } from '@types'
import archiver from 'archiver'
import { exec } from 'child_process'
import { app } from 'electron'
-import Logger from 'electron-log'
import * as fs from 'fs-extra'
import StreamZip from 'node-stream-zip'
import * as path from 'path'
@@ -15,6 +15,8 @@ import S3Storage from './S3Storage'
import WebDav from './WebDav'
import { windowService } from './WindowService'
+const logger = loggerService.withContext('BackupManager')
+
class BackupManager {
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
@@ -58,7 +60,7 @@ class BackupManager {
// 确保根目录权限
await this.forceSetWritable(dirPath)
} catch (error) {
- Logger.error(`权限设置失败:${dirPath}`, error)
+ logger.error(`权限设置失败:${dirPath}`, error)
throw error
}
}
@@ -81,7 +83,7 @@ class BackupManager {
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
- Logger.warn(`权限设置警告:${targetPath}`, error)
+ logger.warn(`权限设置警告:${targetPath}`, error)
}
}
}
@@ -100,7 +102,7 @@ class BackupManager {
// 只在关键阶段记录日志:开始、结束和主要阶段转换点
const logStages = ['preparing', 'writing_data', 'preparing_compression', 'completed']
if (logStages.includes(processData.stage) || processData.progress === 100) {
- Logger.log('[BackupManager] backup progress', processData)
+ logger.debug('backup progress', processData)
}
}
@@ -122,7 +124,7 @@ class BackupManager {
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
- Logger.log('[BackupManager IPC] ', skipBackupFile)
+ logger.debug('BackupManager IPC', skipBackupFile)
if (!skipBackupFile) {
// 复制 Data 目录到临时目录
@@ -143,7 +145,7 @@ class BackupManager {
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
} else {
- Logger.log('[BackupManager] Skip the backup of the file')
+ logger.debug('Skip the backup of the file')
await fs.promises.mkdir(path.join(this.tempDir, 'Data')) // 不创建空 Data 目录会导致 restore 失败
}
@@ -179,7 +181,7 @@ class BackupManager {
}
} catch (error) {
// 仅在出错时记录日志
- Logger.error('[BackupManager] Error calculating totals:', error)
+ logger.error('[BackupManager] Error calculating totals:', error)
}
}
@@ -218,7 +220,7 @@ class BackupManager {
archive.on('error', reject)
archive.on('warning', (err: any) => {
if (err.code !== 'ENOENT') {
- Logger.warn('[BackupManager] Archive warning:', err)
+ logger.warn('[BackupManager] Archive warning:', err)
}
})
@@ -236,10 +238,10 @@ class BackupManager {
await fs.remove(this.tempDir)
onProgress({ stage: 'completed', progress: 100, total: 100 })
- Logger.log('[BackupManager] Backup completed successfully')
+ logger.debug('Backup completed successfully')
return backupedFilePath
} catch (error) {
- Logger.error('[BackupManager] Backup failed:', error)
+ logger.error('[BackupManager] Backup failed:', error)
// 确保清理临时目录
await fs.remove(this.tempDir).catch(() => {})
throw error
@@ -254,7 +256,7 @@ class BackupManager {
// 只在关键阶段记录日志
const logStages = ['preparing', 'extracting', 'extracted', 'reading_data', 'completed']
if (logStages.includes(processData.stage) || processData.progress === 100) {
- Logger.log('[BackupManager] restore progress', processData)
+ logger.debug('restore progress', processData)
}
}
@@ -263,20 +265,20 @@ class BackupManager {
await fs.ensureDir(this.tempDir)
onProgress({ stage: 'preparing', progress: 0, total: 100 })
- Logger.log('[backup] step 1: unzip backup file', this.tempDir)
+ logger.debug('step 1: unzip backup file', this.tempDir)
const zip = new StreamZip.async({ file: backupPath })
onProgress({ stage: 'extracting', progress: 15, total: 100 })
await zip.extract(null, this.tempDir)
onProgress({ stage: 'extracted', progress: 25, total: 100 })
- Logger.log('[backup] step 2: read data.json')
+ logger.debug('step 2: read data.json')
// 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8')
onProgress({ stage: 'reading_data', progress: 35, total: 100 })
- Logger.log('[backup] step 3: restore Data directory')
+ logger.debug('step 3: restore Data directory')
// 恢复 Data 目录
const sourcePath = path.join(this.tempDir, 'Data')
const destPath = getDataPath()
@@ -299,20 +301,20 @@ class BackupManager {
onProgress({ stage: 'copying_files', progress, total: 100 })
})
} else {
- Logger.log('[backup] skipBackupFile is true, skip restoring Data directory')
+ logger.debug('skipBackupFile is true, skip restoring Data directory')
}
- Logger.log('[backup] step 4: clean up temp directory')
+ logger.debug('step 4: clean up temp directory')
// 清理临时目录
await this.setWritableRecursive(this.tempDir)
await fs.remove(this.tempDir)
onProgress({ stage: 'completed', progress: 100, total: 100 })
- Logger.log('[backup] step 5: Restore completed successfully')
+ logger.debug('step 5: Restore completed successfully')
return data
} catch (error) {
- Logger.error('[backup] Restore failed:', error)
+ logger.error('Restore failed:', error)
await fs.remove(this.tempDir).catch(() => {})
throw error
}
@@ -369,7 +371,7 @@ class BackupManager {
return await this.restore(_, backupedFilePath)
} catch (error: any) {
- Logger.error('[backup] Failed to restore from WebDAV:', error)
+ logger.error('Failed to restore from WebDAV:', error)
throw new Error(error.message || 'Failed to restore backup file')
}
}
@@ -389,7 +391,7 @@ class BackupManager {
}))
.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
} catch (error: any) {
- Logger.error('Failed to list WebDAV files:', error)
+ logger.error('Failed to list WebDAV files:', error)
throw new Error(error.message || 'Failed to list backup files')
}
}
@@ -485,7 +487,7 @@ class BackupManager {
const webdavClient = new WebDav(webdavConfig)
return await webdavClient.deleteFile(fileName)
} catch (error: any) {
- Logger.error('Failed to delete WebDAV file:', error)
+ logger.error('Failed to delete WebDAV file:', error)
throw new Error(error.message || 'Failed to delete backup file')
}
}
@@ -507,7 +509,7 @@ class BackupManager {
const backupedFilePath = await this.backup(_, fileName, data, backupDir, localConfig.skipBackupFile)
return backupedFilePath
} catch (error) {
- Logger.error('[BackupManager] Local backup failed:', error)
+ logger.error('[BackupManager] Local backup failed:', error)
throw error
}
}
@@ -521,7 +523,7 @@ class BackupManager {
.slice(0, 14)
const filename = s3Config.fileName || `cherry-studio.backup.${deviceName}.${timestamp}.zip`
- Logger.log(`[BackupManager] Starting S3 backup to ${filename}`)
+ logger.debug(`Starting S3 backup to ${filename}`)
const backupedFilePath = await this.backup(_, filename, data, undefined, s3Config.skipBackupFile)
const s3Client = new S3Storage(s3Config)
@@ -530,10 +532,10 @@ class BackupManager {
const result = await s3Client.putFileContents(filename, fileBuffer)
await fs.remove(backupedFilePath)
- Logger.log(`[BackupManager] S3 backup completed successfully: ${filename}`)
+ logger.debug(`S3 backup completed successfully: ${filename}`)
return result
} catch (error) {
- Logger.error(`[BackupManager] S3 backup failed:`, error)
+ logger.error(`[BackupManager] S3 backup failed:`, error)
await fs.remove(backupedFilePath)
throw error
}
@@ -550,7 +552,7 @@ class BackupManager {
return await this.restore(_, backupPath)
} catch (error) {
- Logger.error('[BackupManager] Local restore failed:', error)
+ logger.error('[BackupManager] Local restore failed:', error)
throw error
}
}
@@ -576,7 +578,7 @@ class BackupManager {
// Sort by modified time, newest first
return result.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
} catch (error) {
- Logger.error('[BackupManager] List local backup files failed:', error)
+ logger.error('[BackupManager] List local backup files failed:', error)
throw error
}
}
@@ -592,7 +594,7 @@ class BackupManager {
await fs.remove(filePath)
return true
} catch (error) {
- Logger.error('[BackupManager] Delete local backup file failed:', error)
+ logger.error('[BackupManager] Delete local backup file failed:', error)
throw error
}
}
@@ -603,7 +605,7 @@ class BackupManager {
await fs.ensureDir(dirPath)
return true
} catch (error) {
- Logger.error('[BackupManager] Set local backup directory failed:', error)
+ logger.error('[BackupManager] Set local backup directory failed:', error)
throw error
}
}
@@ -611,7 +613,7 @@ class BackupManager {
async restoreFromS3(_: Electron.IpcMainInvokeEvent, s3Config: S3Config) {
const filename = s3Config.fileName || 'cherry-studio.backup.zip'
- Logger.log(`[BackupManager] Starting restore from S3: ${filename}`)
+ logger.debug(`Starting restore from S3: ${filename}`)
const s3Client = new S3Storage(s3Config)
try {
@@ -628,10 +630,10 @@ class BackupManager {
writeStream.on('error', (error) => reject(error))
})
- Logger.log(`[BackupManager] S3 restore file downloaded successfully: ${filename}`)
+ logger.debug(`S3 restore file downloaded successfully: ${filename}`)
return await this.restore(_, backupedFilePath)
} catch (error: any) {
- Logger.error('[BackupManager] Failed to restore from S3:', error)
+ logger.error('[BackupManager] Failed to restore from S3:', error)
throw new Error(error.message || 'Failed to restore backup file')
}
}
@@ -655,7 +657,7 @@ class BackupManager {
return files.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
} catch (error: any) {
- Logger.error('Failed to list S3 files:', error)
+ logger.error('Failed to list S3 files:', error)
throw new Error(error.message || 'Failed to list backup files')
}
}
@@ -665,7 +667,7 @@ class BackupManager {
const s3Client = new S3Storage(s3Config)
return await s3Client.deleteFile(fileName)
} catch (error: any) {
- Logger.error('Failed to delete S3 file:', error)
+ logger.error('Failed to delete S3 file:', error)
throw new Error(error.message || 'Failed to delete backup file')
}
}
diff --git a/src/main/services/CopilotService.ts b/src/main/services/CopilotService.ts
index bc331a4468..56898d7c2a 100644
--- a/src/main/services/CopilotService.ts
+++ b/src/main/services/CopilotService.ts
@@ -1,10 +1,12 @@
+import { loggerService } from '@logger'
import { AxiosRequestConfig } from 'axios'
import axios from 'axios'
import { app, safeStorage } from 'electron'
-import Logger from 'electron-log'
import fs from 'fs/promises'
import path from 'path'
+const logger = loggerService.withContext('CopilotService')
+
// 配置常量,集中管理
const CONFIG = {
GITHUB_CLIENT_ID: 'Iv1.b507a08c87ecfe98',
@@ -101,7 +103,7 @@ class CopilotService {
avatar: response.data.avatar_url
}
} catch (error) {
- console.error('Failed to get user information:', error)
+ logger.error('Failed to get user information:', error)
throw new CopilotServiceError('无法获取GitHub用户信息', error)
}
}
@@ -127,7 +129,7 @@ class CopilotService {
return response.data
} catch (error) {
- console.error('Failed to get auth message:', error)
+ logger.error('Failed to get auth message:', error)
throw new CopilotServiceError('无法获取GitHub授权信息', error)
}
}
@@ -169,7 +171,7 @@ class CopilotService {
// 仅在最后一次尝试失败时记录详细错误
const isLastAttempt = attempt === CONFIG.POLLING.MAX_ATTEMPTS - 1
if (isLastAttempt) {
- console.error(`Token polling failed after ${CONFIG.POLLING.MAX_ATTEMPTS} attempts:`, error)
+ logger.error(`Token polling failed after ${CONFIG.POLLING.MAX_ATTEMPTS} attempts:`, error)
}
}
}
@@ -185,7 +187,7 @@ class CopilotService {
const encryptedToken = safeStorage.encryptString(token)
await fs.writeFile(this.tokenFilePath, encryptedToken)
} catch (error) {
- console.error('Failed to save token:', error)
+ logger.error('Failed to save token:', error)
throw new CopilotServiceError('无法保存访问令牌', error)
}
}
@@ -214,7 +216,7 @@ class CopilotService {
return response.data
} catch (error) {
- console.error('Failed to get Copilot token:', error)
+ logger.error('Failed to get Copilot token:', error)
throw new CopilotServiceError('无法获取Copilot令牌,请重新授权', error)
}
}
@@ -227,13 +229,13 @@ class CopilotService {
try {
await fs.access(this.tokenFilePath)
await fs.unlink(this.tokenFilePath)
- Logger.log('Successfully logged out from Copilot')
+ logger.debug('Successfully logged out from Copilot')
} catch (error) {
// 文件不存在不是错误,只是记录一下
- Logger.log('Token file not found, nothing to delete')
+ logger.debug('Token file not found, nothing to delete')
}
} catch (error) {
- console.error('Failed to logout:', error)
+ logger.error('Failed to logout:', error)
throw new CopilotServiceError('无法完成退出登录操作', error)
}
}
diff --git a/src/main/services/DxtService.ts b/src/main/services/DxtService.ts
index f4324d25b4..22bbfaee31 100644
--- a/src/main/services/DxtService.ts
+++ b/src/main/services/DxtService.ts
@@ -1,11 +1,13 @@
+import { loggerService } from '@logger'
import { getMcpDir, getTempDir } from '@main/utils/file'
-import logger from 'electron-log'
import * as fs from 'fs'
import StreamZip from 'node-stream-zip'
import * as os from 'os'
import * as path from 'path'
import { v4 as uuidv4 } from 'uuid'
+const logger = loggerService.withContext('DxtService')
+
// Type definitions
export interface DxtManifest {
dxt_version: string
@@ -174,7 +176,7 @@ class DxtService {
fs.mkdirSync(this.mcpDir, { recursive: true })
}
} catch (error) {
- logger.error('[DxtService] Failed to create directories:', error)
+ logger.error('Failed to create directories:', error)
}
}
@@ -184,7 +186,7 @@ class DxtService {
fs.renameSync(source, destination)
} catch (error) {
// If rename fails (cross-filesystem), use copy + remove
- logger.info('[DxtService] Cross-filesystem move detected, using copy + remove')
+ logger.debug('Cross-filesystem move detected, using copy + remove')
// Ensure parent directory exists
const parentDir = path.dirname(destination)
@@ -230,7 +232,7 @@ class DxtService {
}
// Extract the DXT file (which is a ZIP archive) to a temporary directory
- logger.info('[DxtService] Extracting DXT file:', filePath)
+ logger.debug('Extracting DXT file:', filePath)
const zip = new StreamZip.async({ file: filePath })
await zip.extract(null, tempExtractDir)
@@ -276,14 +278,14 @@ class DxtService {
// Clean up any existing version of this server
if (fs.existsSync(finalExtractDir)) {
- logger.info('[DxtService] Removing existing server directory:', finalExtractDir)
+ logger.debug('Removing existing server directory:', finalExtractDir)
fs.rmSync(finalExtractDir, { recursive: true, force: true })
}
// Move the temporary directory to the final location
// Use recursive copy + remove instead of rename to handle cross-filesystem moves
await this.moveDirectory(tempExtractDir, finalExtractDir)
- logger.info('[DxtService] DXT server extracted to:', finalExtractDir)
+ logger.debug('DXT server extracted to:', finalExtractDir)
// Clean up the uploaded DXT file if it's in temp directory
if (filePath.startsWith(this.tempDir)) {
@@ -305,7 +307,7 @@ class DxtService {
}
const errorMessage = error instanceof Error ? error.message : 'Failed to process DXT file'
- logger.error('[DxtService] DXT upload error:', error)
+ logger.error('DXT upload error:', error)
return {
success: false,
@@ -322,7 +324,7 @@ class DxtService {
// Read the manifest from the DXT server directory
const manifestPath = path.join(dxtPath, 'manifest.json')
if (!fs.existsSync(manifestPath)) {
- logger.error('[DxtService] Manifest not found:', manifestPath)
+ logger.error('Manifest not found:', manifestPath)
return null
}
@@ -330,14 +332,14 @@ class DxtService {
const manifest: DxtManifest = JSON.parse(manifestContent)
if (!manifest.server?.mcp_config) {
- logger.error('[DxtService] No mcp_config found in manifest')
+ logger.error('No mcp_config found in manifest')
return null
}
// Apply platform overrides and variable substitution
const resolvedConfig = applyPlatformOverrides(manifest.server.mcp_config, dxtPath, userConfig)
- logger.info('[DxtService] Resolved MCP config:', {
+ logger.debug('Resolved MCP config:', {
command: resolvedConfig.command,
args: resolvedConfig.args,
env: resolvedConfig.env ? Object.keys(resolvedConfig.env) : undefined
@@ -345,7 +347,7 @@ class DxtService {
return resolvedConfig
} catch (error) {
- logger.error('[DxtService] Failed to resolve MCP config:', error)
+ logger.error('Failed to resolve MCP config:', error)
return null
}
}
@@ -360,7 +362,7 @@ class DxtService {
// First try the sanitized path
if (fs.existsSync(serverDir)) {
- logger.info('[DxtService] Removing DXT server directory:', serverDir)
+ logger.debug('Removing DXT server directory:', serverDir)
fs.rmSync(serverDir, { recursive: true, force: true })
return true
}
@@ -368,15 +370,15 @@ class DxtService {
// Fallback: try with original name in case it was stored differently
const originalServerDir = path.join(this.mcpDir, `server-${serverName}`)
if (fs.existsSync(originalServerDir)) {
- logger.info('[DxtService] Removing DXT server directory:', originalServerDir)
+ logger.debug('Removing DXT server directory:', originalServerDir)
fs.rmSync(originalServerDir, { recursive: true, force: true })
return true
}
- logger.warn('[DxtService] Server directory not found:', serverDir)
+ logger.warn('Server directory not found:', serverDir)
return false
} catch (error) {
- logger.error('[DxtService] Failed to cleanup DXT server:', error)
+ logger.error('Failed to cleanup DXT server:', error)
return false
}
}
@@ -388,7 +390,7 @@ class DxtService {
fs.rmSync(this.tempDir, { recursive: true, force: true })
}
} catch (error) {
- logger.error('[DxtService] Cleanup error:', error)
+ logger.error('Cleanup error:', error)
}
}
}
diff --git a/src/main/services/ExportService.ts b/src/main/services/ExportService.ts
index b17acc9bde..da6949dce9 100644
--- a/src/main/services/ExportService.ts
+++ b/src/main/services/ExportService.ts
@@ -1,6 +1,7 @@
/* eslint-disable no-case-declarations */
// ExportService
+import { loggerService } from '@logger'
import {
AlignmentType,
BorderStyle,
@@ -18,11 +19,11 @@ import {
WidthType
} from 'docx'
import { dialog } from 'electron'
-import Logger from 'electron-log'
import MarkdownIt from 'markdown-it'
import FileStorage from './FileStorage'
+const logger = loggerService.withContext('ExportService')
export class ExportService {
private fileManager: FileStorage
private md: MarkdownIt
@@ -399,10 +400,10 @@ export class ExportService {
if (filePath) {
await this.fileManager.writeFile(_, filePath, buffer)
- Logger.info('[ExportService] Document exported successfully')
+ logger.debug('Document exported successfully')
}
} catch (error) {
- Logger.error('[ExportService] Export to Word failed:', error)
+ logger.error('Export to Word failed:', error)
throw error
}
}
diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts
index 1adecd5733..c3ac8035ad 100644
--- a/src/main/services/FileStorage.ts
+++ b/src/main/services/FileStorage.ts
@@ -1,3 +1,4 @@
+import { loggerService } from '@logger'
import { getFilesDir, getFileType, getTempDir, readTextFileWithAutoEncoding } from '@main/utils/file'
import { documentExts, imageExts, MB } from '@shared/config/constant'
import { FileMetadata } from '@types'
@@ -10,7 +11,6 @@ import {
SaveDialogReturnValue,
shell
} from 'electron'
-import logger from 'electron-log'
import * as fs from 'fs'
import { writeFileSync } from 'fs'
import { readFile } from 'fs/promises'
@@ -21,6 +21,8 @@ import { chdir } from 'process'
import { v4 as uuidv4 } from 'uuid'
import WordExtractor from 'word-extractor'
+const logger = loggerService.withContext('FileStorage')
+
class FileStorage {
private storageDir = getFilesDir()
private tempDir = getTempDir()
@@ -38,7 +40,7 @@ class FileStorage {
fs.mkdirSync(this.tempDir, { recursive: true })
}
} catch (error) {
- logger.error('[FileStorage] Failed to initialize storage directories:', error)
+ logger.error('Failed to initialize storage directories:', error)
throw error
}
}
@@ -136,9 +138,9 @@ class FileStorage {
if (fileSizeInMB > 1) {
try {
await fs.promises.copyFile(sourcePath, destPath)
- logger.info('[FileStorage] Image compressed successfully:', sourcePath)
+ logger.debug('Image compressed successfully:', sourcePath)
} catch (jimpError) {
- logger.error('[FileStorage] Image compression failed:', jimpError)
+ logger.error('Image compression failed:', jimpError)
await fs.promises.copyFile(sourcePath, destPath)
}
} else {
@@ -146,7 +148,7 @@ class FileStorage {
await fs.promises.copyFile(sourcePath, destPath)
}
} catch (error) {
- logger.error('[FileStorage] Image handling failed:', error)
+ logger.error('Image handling failed:', error)
// 错误情况下直接复制原文件
await fs.promises.copyFile(sourcePath, destPath)
}
@@ -188,7 +190,7 @@ class FileStorage {
count: 1
}
- logger.info('[FileStorage] File uploaded:', fileMetadata)
+ logger.debug('File uploaded:', fileMetadata)
return fileMetadata
}
@@ -257,7 +259,7 @@ class FileStorage {
return data
} catch (error) {
chdir(originalCwd)
- logger.error(error)
+ logger.error('Failed to read file:', error)
throw error
}
}
@@ -269,7 +271,7 @@ class FileStorage {
return fs.readFileSync(filePath, 'utf-8')
}
} catch (error) {
- logger.error(error)
+ logger.error('Failed to read file:', error)
throw new Error(`Failed to read file: ${filePath}.`)
}
}
@@ -319,7 +321,7 @@ class FileStorage {
const ext = '.png'
const destPath = path.join(this.storageDir, uuid + ext)
- logger.info('[FileStorage] Saving base64 image:', {
+ logger.debug('Saving base64 image:', {
storageDir: this.storageDir,
destPath,
bufferSize: buffer.length
@@ -346,7 +348,7 @@ class FileStorage {
return fileMetadata
} catch (error) {
- logger.error('[FileStorage] Failed to save base64 image:', error)
+ logger.error('Failed to save base64 image:', error)
throw error
}
}
@@ -560,7 +562,7 @@ class FileStorage {
return fileMetadata
} catch (error) {
- logger.error('[FileStorage] Download file error:', error)
+ logger.error('Download file error:', error)
throw error
}
}
@@ -596,9 +598,9 @@ class FileStorage {
// 复制文件
await fs.promises.copyFile(sourcePath, destPath)
- logger.info('[FileStorage] File copied successfully:', { from: sourcePath, to: destPath })
+ logger.debug('File copied successfully:', { from: sourcePath, to: destPath })
} catch (error) {
- logger.error('[FileStorage] Copy file failed:', error)
+ logger.error('Copy file failed:', error)
throw error
}
}
@@ -606,18 +608,18 @@ class FileStorage {
public writeFileWithId = async (_: Electron.IpcMainInvokeEvent, id: string, content: string): Promise => {
try {
const filePath = path.join(this.storageDir, id)
- logger.info('[FileStorage] Writing file:', filePath)
+ logger.debug('Writing file:', filePath)
// 确保目录存在
if (!fs.existsSync(this.storageDir)) {
- logger.info('[FileStorage] Creating storage directory:', this.storageDir)
+ logger.debug('Creating storage directory:', this.storageDir)
fs.mkdirSync(this.storageDir, { recursive: true })
}
await fs.promises.writeFile(filePath, content, 'utf8')
- logger.info('[FileStorage] File written successfully:', filePath)
+ logger.debug('File written successfully:', filePath)
} catch (error) {
- logger.error('[FileStorage] Failed to write file:', error)
+ logger.error('Failed to write file:', error)
throw error
}
}
diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts
index 736934cd1a..ef6115afec 100644
--- a/src/main/services/KnowledgeService.ts
+++ b/src/main/services/KnowledgeService.ts
@@ -21,6 +21,7 @@ import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { LibSqlDb } from '@cherrystudio/embedjs-libsql'
import { SitemapLoader } from '@cherrystudio/embedjs-loader-sitemap'
import { WebLoader } from '@cherrystudio/embedjs-loader-web'
+import { loggerService } from '@logger'
import OcrProvider from '@main/knowledage/ocr/OcrProvider'
import PreprocessProvider from '@main/knowledage/preprocess/PreprocessProvider'
import Embeddings from '@main/knowledge/embeddings/Embeddings'
@@ -34,9 +35,10 @@ import { MB } from '@shared/config/constant'
import type { LoaderReturn } from '@shared/config/types'
import { IpcChannel } from '@shared/IpcChannel'
import { FileMetadata, KnowledgeBaseParams, KnowledgeItem } from '@types'
-import Logger from 'electron-log'
import { v4 as uuidv4 } from 'uuid'
+const logger = loggerService.withContext('KnowledgeService')
+
export interface KnowledgeBaseAddItemOptions {
base: KnowledgeBaseParams
item: KnowledgeItem
@@ -137,7 +139,7 @@ class KnowledgeService {
.setSearchResultCount(documentCount || 30)
.build()
} catch (e) {
- Logger.error(e)
+ logger.error('Failed to create RAGApplication:', e)
throw new Error(`Failed to create RAGApplication: ${e}`)
}
@@ -190,7 +192,7 @@ class KnowledgeService {
return result
})
.catch((e) => {
- Logger.error(`Error in addFileLoader for ${file.name}: ${e}`)
+ logger.error(`Error in addFileLoader for ${file.name}: ${e}`)
const errorResult: LoaderReturn = {
...KnowledgeService.ERROR_LOADER_RETURN,
message: e.message,
@@ -200,7 +202,7 @@ class KnowledgeService {
return errorResult
})
} catch (e: any) {
- Logger.error(`Preprocessing failed for ${file.name}: ${e}`)
+ logger.error(`Preprocessing failed for ${file.name}: ${e}`)
const errorResult: LoaderReturn = {
...KnowledgeService.ERROR_LOADER_RETURN,
message: e.message,
@@ -256,7 +258,7 @@ class KnowledgeService {
return result
})
.catch((err) => {
- Logger.error(err)
+ logger.error('Failed to add dir loader:', err)
return {
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add dir loader: ${err.message}`,
@@ -306,7 +308,7 @@ class KnowledgeService {
return result
})
.catch((err) => {
- Logger.error(err)
+ logger.error('Failed to add url loader:', err)
return {
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add url loader: ${err.message}`,
@@ -350,7 +352,7 @@ class KnowledgeService {
return result
})
.catch((err) => {
- Logger.error(err)
+ logger.error('Failed to add sitemap loader:', err)
return {
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add sitemap loader: ${err.message}`,
@@ -400,7 +402,7 @@ class KnowledgeService {
}
})
.catch((err) => {
- Logger.error(err)
+ logger.error('Failed to add note loader:', err)
return {
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add note loader: ${err.message}`,
@@ -508,7 +510,7 @@ class KnowledgeService {
}
})
.catch((err) => {
- Logger.error(err)
+ logger.error('Failed to add item:', err)
resolve({
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add item: ${err.message}`,
@@ -523,7 +525,7 @@ class KnowledgeService {
{ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }
): Promise => {
const ragApplication = await this.getRagApplication(base)
- Logger.log(`[ KnowledgeService Remove Item UniqueId: ${uniqueId}]`)
+ logger.debug(`Remove Item UniqueId: ${uniqueId}`)
for (const id of uniqueIds) {
await ragApplication.deleteLoader(id)
}
@@ -569,12 +571,12 @@ class KnowledgeService {
// 首先检查文件是否已经被预处理过
const alreadyProcessed = await provider.checkIfAlreadyProcessed(file)
if (alreadyProcessed) {
- Logger.info(`File already preprocess processed, using cached result: ${file.path}`)
+ logger.debug(`File already preprocess processed, using cached result: ${file.path}`)
return alreadyProcessed
}
// 执行预处理
- Logger.info(`Starting preprocess processing for scanned PDF: ${file.path}`)
+ logger.debug(`Starting preprocess processing for scanned PDF: ${file.path}`)
const { processedFile, quota } = await provider.parseFile(item.id, file)
fileToProcess = processedFile
const mainWindow = windowService.getMainWindow()
@@ -583,7 +585,7 @@ class KnowledgeService {
quota: quota
})
} catch (err) {
- Logger.error(`Preprocess processing failed: ${err}`)
+ logger.error(`Preprocess processing failed: ${err}`)
// 如果预处理失败,使用原始文件
// fileToProcess = file
throw new Error(`Preprocess processing failed: ${err}`)
@@ -605,7 +607,7 @@ class KnowledgeService {
}
throw new Error('No preprocess provider configured')
} catch (err) {
- Logger.error(`Failed to check quota: ${err}`)
+ logger.error(`Failed to check quota: ${err}`)
throw new Error(`Failed to check quota: ${err}`)
}
}
diff --git a/src/main/services/LoggerService.ts b/src/main/services/LoggerService.ts
new file mode 100644
index 0000000000..6bf6e25b57
--- /dev/null
+++ b/src/main/services/LoggerService.ts
@@ -0,0 +1,272 @@
+import type { LogLevel, LogSourceWithContext } from '@shared/config/types'
+import { IpcChannel } from '@shared/IpcChannel'
+import { app, ipcMain } from 'electron'
+import os from 'os'
+import path from 'path'
+import winston from 'winston'
+import DailyRotateFile from 'winston-daily-rotate-file'
+
+import { isDev } from '../constant'
+
+const ANSICOLORS = {
+ RED: '\x1b[31m',
+ GREEN: '\x1b[32m',
+ YELLOW: '\x1b[33m',
+ BLUE: '\x1b[34m',
+ MAGENTA: '\x1b[35m',
+ CYAN: '\x1b[36m',
+ END: '\x1b[0m',
+ BOLD: '\x1b[1m',
+ ITALIC: '\x1b[3m',
+ UNDERLINE: '\x1b[4m'
+}
+function colorText(text: string, color: string) {
+ return ANSICOLORS[color] + text + ANSICOLORS.END
+}
+
+const SYSTEM_INFO = {
+ os: `${os.platform()}-${os.arch()} / ${os.version()}`,
+ hw: `${os.cpus()[0]?.model || 'Unknown CPU'} / ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(2)}GB`
+}
+const APP_VERSION = `v${app?.getVersion?.() || 'unknown'}`
+
+const DEFAULT_LEVEL = isDev ? 'silly' : 'info'
+
+/**
+ * IMPORTANT: How to use LoggerService
+ * please refer to
+ * English: `docs/technical/how-to-use-logger-en.md`
+ * Chinese: `docs/technical/how-to-use-logger-zh.md`
+ */
+export class LoggerService {
+ private static instance: LoggerService
+ private logger: winston.Logger
+
+ private logsDir: string = ''
+
+ private module: string = ''
+ private context: Record = {}
+
+ private constructor() {
+ // Create logs directory path
+ this.logsDir = path.join(app.getPath('userData'), 'logs')
+
+ // Configure transports based on environment
+ const transports: winston.transport[] = []
+
+ //TODO remove when debug is done
+ // transports.push(new winston.transports.Console())
+
+ // Daily rotate file transport for general logs
+ transports.push(
+ new DailyRotateFile({
+ filename: path.join(this.logsDir, 'app.%DATE%.log'),
+ datePattern: 'YYYY-MM-DD',
+ maxSize: '10m',
+ maxFiles: '30d'
+ })
+ )
+
+ // Daily rotate file transport for error logs
+ transports.push(
+ new DailyRotateFile({
+ level: 'warn',
+ filename: path.join(this.logsDir, 'app-error.%DATE%.log'),
+ datePattern: 'YYYY-MM-DD',
+ maxSize: '10m',
+ maxFiles: '60d'
+ })
+ )
+
+ // Configure Winston logger
+ this.logger = winston.createLogger({
+ level: DEFAULT_LEVEL, // Development: all levels, Production: info and above
+ format: winston.format.combine(
+ winston.format.splat(),
+ winston.format.timestamp({
+ format: 'YYYY-MM-DD HH:mm:ss'
+ }),
+ winston.format.errors({ stack: true }),
+ winston.format.json()
+ ),
+ exitOnError: false,
+ transports
+ })
+
+ // Handle transport events
+ this.logger.on('error', (error) => {
+ console.error('LoggerService fatal error:', error)
+ })
+
+ //register ipc handler, for renderer process to log to main process
+ this.registerIpcHandler()
+ }
+
+ public static getInstance(): LoggerService {
+ if (!LoggerService.instance) {
+ LoggerService.instance = new LoggerService()
+ }
+ return LoggerService.instance
+ }
+
+ public withContext(module: string, context?: Record): LoggerService {
+ const newLogger = Object.create(this)
+
+ // Copy all properties from the base logger
+ newLogger.logger = this.logger
+ newLogger.module = module
+ newLogger.context = { ...this.context, ...context }
+
+ return newLogger
+ }
+
+ public finish() {
+ this.logger.end()
+ }
+
+ private processLog(source: LogSourceWithContext, level: LogLevel, message: string, meta: any[]): void {
+ if (isDev) {
+ const datetimeColored = colorText(
+ new Date().toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ fractionalSecondDigits: 3,
+ hour12: false
+ }),
+ 'CYAN'
+ )
+
+ console.log('processLog', source.process, this.module, this.context)
+
+ let moduleString = ''
+ if (source.process === 'main') {
+ moduleString = this.module ? ` [${colorText(this.module, 'UNDERLINE')}] ` : ' '
+ } else {
+ const combineString = `${source.window}:${source.module}`
+ moduleString = ` [${colorText(combineString, 'UNDERLINE')}] `
+ }
+
+ switch (level) {
+ case 'error':
+ console.error(
+ `${datetimeColored} ${colorText(colorText('', 'RED'), 'BOLD')}${moduleString}${message}`,
+ ...meta
+ )
+ break
+ case 'warn':
+ console.warn(
+ `${datetimeColored} ${colorText(colorText('', 'YELLOW'), 'BOLD')}${moduleString}${message}`,
+ ...meta
+ )
+ break
+ case 'info':
+ console.info(
+ `${datetimeColored} ${colorText(colorText('', 'GREEN'), 'BOLD')}${moduleString}${message}`,
+ ...meta
+ )
+ break
+ case 'debug':
+ console.debug(
+ `${datetimeColored} ${colorText(colorText('', 'BLUE'), 'BOLD')}${moduleString}${message}`,
+ ...meta
+ )
+ break
+ case 'verbose':
+ console.log(`${datetimeColored} ${colorText('', 'BOLD')}${moduleString}${message}`, ...meta)
+ break
+ case 'silly':
+ console.log(`${datetimeColored} ${colorText('', 'BOLD')}${moduleString}${message}`, ...meta)
+ break
+ }
+ }
+
+ // add source information to meta
+ // renderer process has its own module and context, do not use this.module and this.context
+ const sourceWithContext: LogSourceWithContext = source
+ if (source.process === 'main') {
+ sourceWithContext.module = this.module
+ if (Object.keys(this.context).length > 0) {
+ sourceWithContext.context = this.context
+ }
+ }
+ meta.push(sourceWithContext)
+
+ // add extra system information for error and warn levels
+ if (level === 'error' || level === 'warn') {
+ const extra = {
+ sys: SYSTEM_INFO,
+ appver: APP_VERSION
+ }
+
+ meta.push(extra)
+ }
+
+ this.logger.log(level, message, ...meta)
+ }
+
+ public error(message: string, ...data: any[]): void {
+ this.processMainLog('error', message, data)
+ }
+ public warn(message: string, ...data: any[]): void {
+ this.processMainLog('warn', message, data)
+ }
+ public info(message: string, ...data: any[]): void {
+ this.processMainLog('info', message, data)
+ }
+ public verbose(message: string, ...data: any[]): void {
+ this.processMainLog('verbose', message, data)
+ }
+ public debug(message: string, ...data: any[]): void {
+ this.processMainLog('debug', message, data)
+ }
+ public silly(message: string, ...data: any[]): void {
+ this.processMainLog('silly', message, data)
+ }
+
+ private processMainLog(level: LogLevel, message: string, data: any[]): void {
+ this.processLog({ process: 'main' }, level, message, data)
+ }
+
+ // bind original this to become a callback function
+ private processRendererLog = (source: LogSourceWithContext, level: LogLevel, message: string, data: any[]): void => {
+ this.processLog(source, level, message, data)
+ }
+
+ // Additional utility methods
+ public setLevel(level: string): void {
+ this.logger.level = level
+ }
+
+ public getLevel(): string {
+ return this.logger.level
+ }
+
+ // Method to reset log level to environment default
+ public resetLevel(): void {
+ this.setLevel(DEFAULT_LEVEL)
+ }
+
+ // Method to get the underlying Winston logger instance
+ public getBaseLogger(): winston.Logger {
+ return this.logger
+ }
+
+ public getLogsDir(): string {
+ return this.logsDir
+ }
+
+ private registerIpcHandler(): void {
+ ipcMain.handle(
+ IpcChannel.App_LogToMain,
+ (_, source: LogSourceWithContext, level: LogLevel, message: string, data: any[]) => {
+ this.processRendererLog(source, level, message, data)
+ }
+ )
+ }
+}
+
+export const loggerService = LoggerService.getInstance()
diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts
index 61959f1676..4e93664d7d 100644
--- a/src/main/services/MCPService.ts
+++ b/src/main/services/MCPService.ts
@@ -2,6 +2,7 @@ import crypto from 'node:crypto'
import os from 'node:os'
import path from 'node:path'
+import { loggerService } from '@logger'
import { createInMemoryMCPServer } from '@main/mcpServers/factory'
import { makeSureDirExists } from '@main/utils'
import { buildFunctionCallToolName } from '@main/utils/mcp'
@@ -35,7 +36,6 @@ import {
MCPTool
} from '@types'
import { app } from 'electron'
-import Logger from 'electron-log'
import { EventEmitter } from 'events'
import { memoize } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
@@ -49,6 +49,8 @@ import getLoginShellEnvironment from './mcp/shell-env'
// Generic type for caching wrapped functions
type CachedFunction = (...args: T) => Promise
+const logger = loggerService.withContext('MCPService')
+
/**
* Higher-order function to add caching capability to any async function
* @param fn The original function to be wrapped with caching
@@ -67,7 +69,7 @@ function withCache(
const cacheKey = getCacheKey(...args)
if (CacheService.has(cacheKey)) {
- Logger.info(`${logPrefix} loaded from cache`)
+ logger.debug(`${logPrefix} loaded from cache`)
const cachedData = CacheService.get(cacheKey)
if (cachedData) {
return cachedData
@@ -130,7 +132,7 @@ class McpService {
try {
// Check if the existing client is still connected
const pingResult = await existingClient.ping()
- Logger.info(`[MCP] Ping result for ${server.name}:`, pingResult)
+ logger.debug(`Ping result for ${server.name}:`, pingResult)
// If the ping fails, remove the client from the cache
// and create a new one
if (!pingResult) {
@@ -139,7 +141,7 @@ class McpService {
return existingClient
}
} catch (error: any) {
- Logger.error(`[MCP] Error pinging server ${server.name}:`, error?.message)
+ logger.error(`Error pinging server ${server.name}:`, error?.message)
this.clients.delete(serverKey)
}
}
@@ -165,15 +167,15 @@ class McpService {
> => {
// Create appropriate transport based on configuration
if (server.type === 'inMemory') {
- Logger.info(`[MCP] Using in-memory transport for server: ${server.name}`)
+ logger.debug(`Using in-memory transport for server: ${server.name}`)
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
// start the in-memory server with the given name and environment variables
const inMemoryServer = createInMemoryMCPServer(server.name, args, server.env || {})
try {
await inMemoryServer.connect(serverTransport)
- Logger.info(`[MCP] In-memory server started: ${server.name}`)
+ logger.debug(`In-memory server started: ${server.name}`)
} catch (error: Error | any) {
- Logger.error(`[MCP] Error starting in-memory server: ${error}`)
+ logger.error(`Error starting in-memory server: ${error}`)
throw new Error(`Failed to start in-memory server: ${error.message}`)
}
// set the client transport to the client
@@ -201,7 +203,7 @@ class McpService {
headers['Authorization'] = `Bearer ${tokens.access_token}`
}
} catch (error) {
- Logger.error('Failed to fetch tokens:', error)
+ logger.error('Failed to fetch tokens:', error)
}
}
@@ -231,15 +233,15 @@ class McpService {
...server.env,
...resolvedConfig.env
}
- Logger.info(`[MCP] Using resolved DXT config - command: ${cmd}, args: ${args?.join(' ')}`)
+ logger.debug(`Using resolved DXT config - command: ${cmd}, args: ${args?.join(' ')}`)
} else {
- Logger.warn(`[MCP] Failed to resolve DXT config for ${server.name}, falling back to manifest values`)
+ logger.warn(`Failed to resolve DXT config for ${server.name}, falling back to manifest values`)
}
}
if (server.command === 'npx') {
cmd = await getBinaryPath('bun')
- Logger.info(`[MCP] Using command: ${cmd}`)
+ logger.debug(`Using command: ${cmd}`)
// add -x to args if args exist
if (args && args.length > 0) {
@@ -274,7 +276,7 @@ class McpService {
}
}
- Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
+ logger.debug(`Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
// Logger.info(`[MCP] Environment variables for server:`, server.env)
const loginShellEnv = await this.getLoginShellEnv()
@@ -296,12 +298,12 @@ class McpService {
// For DXT servers, set the working directory to the extracted path
if (server.dxtPath) {
transportOptions.cwd = server.dxtPath
- Logger.info(`[MCP] Setting working directory for DXT server: ${server.dxtPath}`)
+ logger.debug(`Setting working directory for DXT server: ${server.dxtPath}`)
}
const stdioTransport = new StdioClientTransport(transportOptions)
stdioTransport.stderr?.on('data', (data) =>
- Logger.info(`[MCP] Stdio stderr for server: ${server.name} `, data.toString())
+ logger.debug(`Stdio stderr for server: ${server.name}` + data.toString())
)
return stdioTransport
} else {
@@ -310,7 +312,7 @@ class McpService {
}
const handleAuth = async (client: Client, transport: SSEClientTransport | StreamableHTTPClientTransport) => {
- Logger.info(`[MCP] Starting OAuth flow for server: ${server.name}`)
+ logger.debug(`Starting OAuth flow for server: ${server.name}`)
// Create an event emitter for the OAuth callback
const events = new EventEmitter()
@@ -323,27 +325,27 @@ class McpService {
// Set a timeout to close the callback server
const timeoutId = setTimeout(() => {
- Logger.warn(`[MCP] OAuth flow timed out for server: ${server.name}`)
+ logger.warn(`OAuth flow timed out for server: ${server.name}`)
callbackServer.close()
}, 300000) // 5 minutes timeout
try {
// Wait for the authorization code
const authCode = await callbackServer.waitForAuthCode()
- Logger.info(`[MCP] Received auth code: ${authCode}`)
+ logger.debug(`Received auth code: ${authCode}`)
// Complete the OAuth flow
await transport.finishAuth(authCode)
- Logger.info(`[MCP] OAuth flow completed for server: ${server.name}`)
+ logger.debug(`OAuth flow completed for server: ${server.name}`)
const newTransport = await initTransport()
// Try to connect again
await client.connect(newTransport)
- Logger.info(`[MCP] Successfully authenticated with server: ${server.name}`)
+ logger.debug(`Successfully authenticated with server: ${server.name}`)
} catch (oauthError) {
- Logger.error(`[MCP] OAuth authentication failed for server ${server.name}:`, oauthError)
+ logger.error(`OAuth authentication failed for server ${server.name}:`, oauthError)
throw new Error(
`OAuth authentication failed: ${oauthError instanceof Error ? oauthError.message : String(oauthError)}`
)
@@ -363,7 +365,7 @@ class McpService {
error instanceof Error &&
(error.name === 'UnauthorizedError' || error.message.includes('Unauthorized'))
) {
- Logger.info(`[MCP] Authentication required for server: ${server.name}`)
+ logger.debug(`Authentication required for server: ${server.name}`)
await handleAuth(client, transport as SSEClientTransport | StreamableHTTPClientTransport)
} else {
throw error
@@ -379,10 +381,16 @@ class McpService {
// Clear existing cache to ensure fresh data
this.clearServerCache(serverKey)
- Logger.info(`[MCP] Activated server: ${server.name}`)
+ // Set up notification handlers
+ this.setupNotificationHandlers(client, server)
+
+ // Clear existing cache to ensure fresh data
+ this.clearServerCache(serverKey)
+
+ logger.debug(`Activated server: ${server.name}`)
return client
} catch (error: any) {
- Logger.error(`[MCP] Error activating server ${server.name}:`, error?.message)
+ logger.error(`Error activating server ${server.name}:`, error?.message)
throw new Error(`[MCP] Error activating server ${server.name}: ${error.message}`)
}
} finally {
@@ -406,50 +414,50 @@ class McpService {
try {
// Set up tools list changed notification handler
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
- Logger.info(`[MCP] Tools list changed for server: ${server.name}`)
+ logger.debug(`Tools list changed for server: ${server.name}`)
// Clear tools cache
CacheService.remove(`mcp:list_tool:${serverKey}`)
})
// Set up resources list changed notification handler
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
- Logger.info(`[MCP] Resources list changed for server: ${server.name}`)
+ logger.debug(`Resources list changed for server: ${server.name}`)
// Clear resources cache
CacheService.remove(`mcp:list_resources:${serverKey}`)
})
// Set up prompts list changed notification handler
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
- Logger.info(`[MCP] Prompts list changed for server: ${server.name}`)
+ logger.debug(`Prompts list changed for server: ${server.name}`)
// Clear prompts cache
CacheService.remove(`mcp:list_prompts:${serverKey}`)
})
// Set up resource updated notification handler
client.setNotificationHandler(ResourceUpdatedNotificationSchema, async () => {
- Logger.info(`[MCP] Resource updated for server: ${server.name}`)
+ logger.debug(`Resource updated for server: ${server.name}`)
// Clear resource-specific caches
this.clearResourceCaches(serverKey)
})
// Set up progress notification handler
client.setNotificationHandler(ProgressNotificationSchema, async (notification) => {
- Logger.info(`[MCP] Progress notification received for server: ${server.name}`, notification.params)
+ logger.debug(`Progress notification received for server: ${server.name}`, notification.params)
})
// Set up cancelled notification handler
client.setNotificationHandler(CancelledNotificationSchema, async (notification) => {
- Logger.info(`[MCP] Operation cancelled for server: ${server.name}`, notification.params)
+ logger.debug(`Operation cancelled for server: ${server.name}`, notification.params)
})
// Set up logging message notification handler
client.setNotificationHandler(LoggingMessageNotificationSchema, async (notification) => {
- Logger.info(`[MCP] Message from server ${server.name}:`, notification.params)
+ logger.debug(`Message from server ${server.name}:`, notification.params)
})
- Logger.info(`[MCP] Set up notification handlers for server: ${server.name}`)
+ logger.debug(`Set up notification handlers for server: ${server.name}`)
} catch (error) {
- Logger.error(`[MCP] Failed to set up notification handlers for server ${server.name}:`, error)
+ logger.error(`Failed to set up notification handlers for server ${server.name}:`, error)
}
}
@@ -467,7 +475,7 @@ class McpService {
CacheService.remove(`mcp:list_tool:${serverKey}`)
CacheService.remove(`mcp:list_prompts:${serverKey}`)
CacheService.remove(`mcp:list_resources:${serverKey}`)
- Logger.info(`[MCP] Cleared all caches for server: ${serverKey}`)
+ logger.debug(`Cleared all caches for server: ${serverKey}`)
}
async closeClient(serverKey: string) {
@@ -475,18 +483,18 @@ class McpService {
if (client) {
// Remove the client from the cache
await client.close()
- Logger.info(`[MCP] Closed server: ${serverKey}`)
+ logger.debug(`Closed server: ${serverKey}`)
this.clients.delete(serverKey)
// Clear all caches for this server
this.clearServerCache(serverKey)
} else {
- Logger.warn(`[MCP] No client found for server: ${serverKey}`)
+ logger.warn(`No client found for server: ${serverKey}`)
}
}
async stopServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
const serverKey = this.getServerKey(server)
- Logger.info(`[MCP] Stopping server: ${server.name}`)
+ logger.debug(`Stopping server: ${server.name}`)
await this.closeClient(serverKey)
}
@@ -502,16 +510,16 @@ class McpService {
try {
const cleaned = this.dxtService.cleanupDxtServer(server.name)
if (cleaned) {
- Logger.info(`[MCP] Cleaned up DXT server directory for: ${server.name}`)
+ logger.debug(`Cleaned up DXT server directory for: ${server.name}`)
}
} catch (error) {
- Logger.error(`[MCP] Failed to cleanup DXT server: ${server.name}`, error)
+ logger.error(`Failed to cleanup DXT server: ${server.name}`, error)
}
}
}
async restartServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
- Logger.info(`[MCP] Restarting server: ${server.name}`)
+ logger.debug(`Restarting server: ${server.name}`)
const serverKey = this.getServerKey(server)
await this.closeClient(serverKey)
// Clear cache before restarting to ensure fresh data
@@ -524,7 +532,7 @@ class McpService {
try {
await this.closeClient(key)
} catch (error: any) {
- Logger.error(`[MCP] Failed to close client: ${error?.message}`)
+ logger.error(`Failed to close client: ${error?.message}`)
}
}
}
@@ -533,9 +541,9 @@ class McpService {
* Check connectivity for an MCP server
*/
public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise {
- Logger.info(`[MCP] Checking connectivity for server: ${server.name}`)
+ logger.debug(`Checking connectivity for server: ${server.name}`)
try {
- Logger.info(`[MCP] About to call initClient for server: ${server.name}`, { hasInitClient: !!this.initClient })
+ logger.debug(`About to call initClient for server: ${server.name}`, { hasInitClient: !!this.initClient })
if (!this.initClient) {
throw new Error('initClient method is not available')
@@ -544,10 +552,10 @@ class McpService {
const client = await this.initClient(server)
// Attempt to list tools as a way to check connectivity
await client.listTools()
- Logger.info(`[MCP] Connectivity check successful for server: ${server.name}`)
+ logger.debug(`Connectivity check successful for server: ${server.name}`)
return true
} catch (error) {
- Logger.error(`[MCP] Connectivity check failed for server: ${server.name}`, error)
+ logger.error(`Connectivity check failed for server: ${server.name}`, error)
// Close the client if connectivity check fails to ensure a clean state for the next attempt
const serverKey = this.getServerKey(server)
await this.closeClient(serverKey)
@@ -556,7 +564,7 @@ class McpService {
}
private async listToolsImpl(server: MCPServer): Promise {
- Logger.info(`[MCP] Listing tools for server: ${server.name}`)
+ logger.debug(`Listing tools for server: ${server.name}`)
const client = await this.initClient(server)
try {
const { tools } = await client.listTools()
@@ -572,7 +580,7 @@ class McpService {
})
return serverTools
} catch (error: any) {
- Logger.error(`[MCP] Failed to list tools for server: ${server.name}`, error?.message)
+ logger.error(`Failed to list tools for server: ${server.name}`, error?.message)
return []
}
}
@@ -603,12 +611,12 @@ class McpService {
this.activeToolCalls.set(toolCallId, abortController)
try {
- Logger.info('[MCP] Calling:', server.name, name, args, 'callId:', toolCallId)
+ logger.debug('Calling:', server.name, name, args, 'callId:', toolCallId)
if (typeof args === 'string') {
try {
args = JSON.parse(args)
} catch (e) {
- Logger.error('[MCP] args parse error', args)
+ logger.error('args parse error', args)
}
}
const client = await this.initClient(server)
@@ -622,7 +630,7 @@ class McpService {
})
return result as MCPCallToolResponse
} catch (error) {
- Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
+ logger.error(`Error calling tool ${name} on ${server.name}:`, error)
throw error
} finally {
this.activeToolCalls.delete(toolCallId)
@@ -643,7 +651,7 @@ class McpService {
*/
private async listPromptsImpl(server: MCPServer): Promise {
const client = await this.initClient(server)
- Logger.info(`[MCP] Listing prompts for server: ${server.name}`)
+ logger.debug(`Listing prompts for server: ${server.name}`)
try {
const { prompts } = await client.listPrompts()
return prompts.map((prompt: any) => ({
@@ -655,7 +663,7 @@ class McpService {
} catch (error: any) {
// -32601 is the code for the method not found
if (error?.code !== -32601) {
- Logger.error(`[MCP] Failed to list prompts for server: ${server.name}`, error?.message)
+ logger.error(`Failed to list prompts for server: ${server.name}`, error?.message)
}
return []
}
@@ -685,7 +693,7 @@ class McpService {
name: string,
args?: Record
): Promise {
- Logger.info(`[MCP] Getting prompt ${name} from server: ${server.name}`)
+ logger.debug(`Getting prompt ${name} from server: ${server.name}`)
const client = await this.initClient(server)
return await client.getPrompt({ name, arguments: args })
}
@@ -715,7 +723,7 @@ class McpService {
*/
private async listResourcesImpl(server: MCPServer): Promise {
const client = await this.initClient(server)
- Logger.info(`[MCP] Listing resources for server: ${server.name}`)
+ logger.debug(`Listing resources for server: ${server.name}`)
try {
const result = await client.listResources()
const resources = result.resources || []
@@ -727,7 +735,7 @@ class McpService {
} catch (error: any) {
// -32601 is the code for the method not found
if (error?.code !== -32601) {
- Logger.error(`[MCP] Failed to list resources for server: ${server.name}`, error?.message)
+ logger.error(`Failed to list resources for server: ${server.name}`, error?.message)
}
return []
}
@@ -753,7 +761,7 @@ class McpService {
* Get a specific resource from an MCP server (implementation)
*/
private async getResourceImpl(server: MCPServer, uri: string): Promise {
- Logger.info(`[MCP] Getting resource ${uri} from server: ${server.name}`)
+ logger.debug(`Getting resource ${uri} from server: ${server.name}`)
const client = await this.initClient(server)
try {
const result = await client.readResource({ uri: uri })
@@ -771,7 +779,7 @@ class McpService {
contents: contents
}
} catch (error: Error | any) {
- Logger.error(`[MCP] Failed to get resource ${uri} from server: ${server.name}`, error.message)
+ logger.error(`Failed to get resource ${uri} from server: ${server.name}`, error.message)
throw new Error(`Failed to get resource ${uri} from server: ${server.name}: ${error.message}`)
}
}
@@ -801,10 +809,10 @@ class McpService {
const pathSeparator = process.platform === 'win32' ? ';' : ':'
const cherryBinPath = path.join(os.homedir(), '.cherrystudio', 'bin')
loginEnv.PATH = `${loginEnv.PATH}${pathSeparator}${cherryBinPath}`
- Logger.info('[MCP] Successfully fetched login shell environment variables:')
+ logger.debug('Successfully fetched login shell environment variables:')
return loginEnv
} catch (error) {
- Logger.error('[MCP] Failed to fetch login shell environment variables:', error)
+ logger.error('Failed to fetch login shell environment variables:', error)
return {}
}
})
@@ -823,10 +831,10 @@ class McpService {
if (activeToolCall) {
activeToolCall.abort()
this.activeToolCalls.delete(callId)
- Logger.info(`[MCP] Aborted tool call: ${callId}`)
+ logger.debug(`Aborted tool call: ${callId}`)
return true
} else {
- Logger.warn(`[MCP] No active tool call found for callId: ${callId}`)
+ logger.warn(`No active tool call found for callId: ${callId}`)
return false
}
}
@@ -836,22 +844,22 @@ class McpService {
*/
public async getServerVersion(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise {
try {
- Logger.info(`[MCP] Getting server version for: ${server.name}`)
+ logger.debug(`Getting server version for: ${server.name}`)
const client = await this.initClient(server)
// Try to get server information which may include version
const serverInfo = client.getServerVersion()
- Logger.info(`[MCP] Server info for ${server.name}:`, serverInfo)
+ logger.debug(`Server info for ${server.name}:`, serverInfo)
if (serverInfo && serverInfo.version) {
- Logger.info(`[MCP] Server version for ${server.name}: ${serverInfo.version}`)
+ logger.debug(`Server version for ${server.name}: ${serverInfo.version}`)
return serverInfo.version
}
- Logger.warn(`[MCP] No version information available for server: ${server.name}`)
+ logger.warn(`No version information available for server: ${server.name}`)
return null
} catch (error: any) {
- Logger.error(`[MCP] Failed to get server version for ${server.name}:`, error?.message)
+ logger.error(`Failed to get server version for ${server.name}:`, error?.message)
return null
}
}
diff --git a/src/main/services/NutstoreService.ts b/src/main/services/NutstoreService.ts
index 5f256f52c3..ac8cb3b70d 100644
--- a/src/main/services/NutstoreService.ts
+++ b/src/main/services/NutstoreService.ts
@@ -1,5 +1,6 @@
import path from 'node:path'
+import { loggerService } from '@logger'
import { NUTSTORE_HOST } from '@shared/config/nutstore'
import { XMLParser } from 'fast-xml-parser'
import { isNil, partial } from 'lodash'
@@ -7,6 +8,8 @@ import { type FileStat } from 'webdav'
import { createOAuthUrl, decryptSecret } from '../integration/nutstore/sso/lib/index.mjs'
+const logger = loggerService.withContext('NutstoreService')
+
interface OAuthResponse {
username: string
userid: string
@@ -45,7 +48,7 @@ export async function decryptToken(token: string) {
})
return JSON.parse(decrypted) as OAuthResponse
} catch (error) {
- console.error('解密失败:', error)
+ logger.error('Failed to decrypt token:', error)
return null
}
}
diff --git a/src/main/services/ObsidianVaultService.ts b/src/main/services/ObsidianVaultService.ts
index 0f9b33c475..894bdc8864 100644
--- a/src/main/services/ObsidianVaultService.ts
+++ b/src/main/services/ObsidianVaultService.ts
@@ -1,8 +1,9 @@
+import { loggerService } from '@logger'
import { app } from 'electron'
-import Logger from 'electron-log'
import fs from 'fs'
import path from 'path'
+const logger = loggerService.withContext('ObsidianVaultService')
interface VaultInfo {
path: string
name: string
@@ -56,7 +57,7 @@ class ObsidianVaultService {
name: vault.name || path.basename(vault.path)
}))
} catch (error) {
- console.error('获取Obsidian Vault失败:', error)
+ logger.error('Failed to get Obsidian Vault:', error)
return []
}
}
@@ -70,20 +71,20 @@ class ObsidianVaultService {
try {
// 检查vault路径是否存在
if (!fs.existsSync(vaultPath)) {
- console.error('Vault路径不存在:', vaultPath)
+ logger.error('Vault path does not exist:', vaultPath)
return []
}
// 检查是否是目录
const stats = fs.statSync(vaultPath)
if (!stats.isDirectory()) {
- console.error('Vault路径不是一个目录:', vaultPath)
+ logger.error('Vault path is not a directory:', vaultPath)
return []
}
this.traverseDirectory(vaultPath, '', results)
} catch (error) {
- console.error('读取Vault文件夹结构失败:', error)
+ logger.error('Failed to read Vault folder structure:', error)
}
return results
@@ -105,7 +106,7 @@ class ObsidianVaultService {
// 确保目录存在且可访问
if (!fs.existsSync(dirPath)) {
- console.error('目录不存在:', dirPath)
+ logger.error('Directory does not exist:', dirPath)
return
}
@@ -113,7 +114,7 @@ class ObsidianVaultService {
try {
items = fs.readdirSync(dirPath, { withFileTypes: true })
} catch (err) {
- console.error(`无法读取目录 ${dirPath}:`, err)
+ logger.error(`Failed to read directory ${dirPath}:`, err)
return
}
@@ -138,7 +139,7 @@ class ObsidianVaultService {
}
}
} catch (error) {
- console.error(`遍历目录出错 ${dirPath}:`, error)
+ logger.error(`Failed to traverse directory ${dirPath}:`, error)
}
}
@@ -152,14 +153,14 @@ class ObsidianVaultService {
const vault = vaults.find((v) => v.name === vaultName)
if (!vault) {
- console.error('未找到指定名称的Vault:', vaultName)
+ logger.error('Vault not found:', vaultName)
return []
}
- Logger.log('获取Vault文件结构:', vault.name, vault.path)
+ logger.debug('Get Vault file structure:', vault.name, vault.path)
return this.getVaultStructure(vault.path)
} catch (error) {
- console.error('获取Vault文件结构时发生错误:', error)
+ logger.error('Failed to get Vault file structure:', error)
return []
}
}
diff --git a/src/main/services/ProtocolClient.ts b/src/main/services/ProtocolClient.ts
index cac0983fd6..086f51b154 100644
--- a/src/main/services/ProtocolClient.ts
+++ b/src/main/services/ProtocolClient.ts
@@ -3,13 +3,15 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import { promisify } from 'node:util'
+import { loggerService } from '@logger'
import { app } from 'electron'
-import Logger from 'electron-log'
import { handleProvidersProtocolUrl } from './urlschema/handle-providers'
import { handleMcpProtocolUrl } from './urlschema/mcp-install'
import { windowService } from './WindowService'
+const logger = loggerService.withContext('ProtocolClient')
+
export const CHERRY_STUDIO_PROTOCOL = 'cherrystudio'
export function registerProtocolClient(app: Electron.App) {
@@ -65,12 +67,12 @@ export async function setupAppImageDeepLink(): Promise {
return
}
- Logger.info('AppImage environment detected on Linux, setting up deep link.')
+ logger.debug('AppImage environment detected on Linux, setting up deep link.')
try {
const appPath = app.getPath('exe')
if (!appPath) {
- Logger.error('Could not determine App path.')
+ logger.error('Could not determine App path.')
return
}
@@ -95,24 +97,24 @@ NoDisplay=true
// Write the .desktop file (overwrite if exists)
await fs.writeFile(desktopFilePath, desktopFileContent, 'utf-8')
- Logger.info(`Created/Updated desktop file: ${desktopFilePath}`)
+ logger.debug(`Created/Updated desktop file: ${desktopFilePath}`)
// Update the desktop database
// It's important to update the database for the changes to take effect
try {
const { stdout, stderr } = await execAsync(`update-desktop-database ${escapePathForExec(applicationsDir)}`)
if (stderr) {
- Logger.warn(`update-desktop-database stderr: ${stderr}`)
+ logger.warn(`update-desktop-database stderr: ${stderr}`)
}
- Logger.info(`update-desktop-database stdout: ${stdout}`)
- Logger.info('Desktop database updated successfully.')
+ logger.debug(`update-desktop-database stdout: ${stdout}`)
+ logger.debug('Desktop database updated successfully.')
} catch (updateError) {
- Logger.error('Failed to update desktop database:', updateError)
+ logger.error('Failed to update desktop database:', updateError)
// Continue even if update fails, as the file is still created.
}
} catch (error) {
// Log the error but don't prevent the app from starting
- Logger.error('Failed to setup AppImage deep link:', error)
+ logger.error('Failed to setup AppImage deep link:', error)
}
}
diff --git a/src/main/services/ProxyManager.ts b/src/main/services/ProxyManager.ts
index a2936e37dc..60bef773c4 100644
--- a/src/main/services/ProxyManager.ts
+++ b/src/main/services/ProxyManager.ts
@@ -1,6 +1,6 @@
+import { loggerService } from '@logger'
import axios from 'axios'
import { app, ProxyConfig, session } from 'electron'
-import Logger from 'electron-log'
import { socksDispatcher } from 'fetch-socks'
import http from 'http'
import https from 'https'
@@ -8,6 +8,8 @@ import { getSystemProxy } from 'os-proxy-config'
import { ProxyAgent } from 'proxy-agent'
import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici'
+const logger = loggerService.withContext('ProxyManager')
+
export class ProxyManager {
private config: ProxyConfig = { mode: 'direct' }
private systemProxyInterval: NodeJS.Timeout | null = null
@@ -59,7 +61,7 @@ export class ProxyManager {
}
async configureProxy(config: ProxyConfig): Promise {
- Logger.info('configureProxy', config.mode, config.proxyRules)
+ logger.info('configureProxy', config.mode, config.proxyRules)
if (this.isSettingProxy) {
return
}
@@ -68,7 +70,7 @@ export class ProxyManager {
try {
if (config?.mode === this.config?.mode && config?.proxyRules === this.config?.proxyRules) {
- Logger.info('proxy config is the same, skip configure')
+ logger.info('proxy config is the same, skip configure')
return
}
@@ -77,7 +79,7 @@ export class ProxyManager {
if (config.mode === 'system') {
const currentProxy = await getSystemProxy()
if (currentProxy) {
- Logger.info('current system proxy', currentProxy.proxyUrl)
+ logger.info('current system proxy', currentProxy.proxyUrl)
this.config.proxyRules = currentProxy.proxyUrl.toLowerCase()
this.monitorSystemProxy()
} else {
@@ -88,7 +90,7 @@ export class ProxyManager {
this.setGlobalProxy()
} catch (error) {
- Logger.error('Failed to config proxy:', error)
+ logger.error('Failed to config proxy:', error)
throw error
} finally {
this.isSettingProxy = false
diff --git a/src/main/services/ReduxService.ts b/src/main/services/ReduxService.ts
index 3cddd0e947..44ff02c0dc 100644
--- a/src/main/services/ReduxService.ts
+++ b/src/main/services/ReduxService.ts
@@ -1,3 +1,4 @@
+import { loggerService } from '@logger'
import { IpcChannel } from '@shared/IpcChannel'
import { ipcMain } from 'electron'
import { EventEmitter } from 'events'
@@ -7,6 +8,8 @@ import { windowService } from './WindowService'
type StoreValue = any
type Unsubscribe = () => void
+const logger = loggerService.withContext('ReduxService')
+
export class ReduxService extends EventEmitter {
private stateCache: any = {}
private isReady = false
@@ -65,7 +68,7 @@ export class ReduxService extends EventEmitter {
const selectorFn = new Function('state', `return ${selector}`)
return selectorFn(this.stateCache)
} catch (error) {
- console.error('Failed to select from cache:', error)
+ logger.error('Failed to select from cache:', error)
return undefined
}
}
@@ -94,7 +97,7 @@ export class ReduxService extends EventEmitter {
})()
`)
} catch (error) {
- console.error('Failed to select store value:', error)
+ logger.error('Failed to select store value:', error)
throw error
}
}
@@ -111,7 +114,7 @@ export class ReduxService extends EventEmitter {
window.store.dispatch(${JSON.stringify(action)})
`)
} catch (error) {
- console.error('Failed to dispatch action:', error)
+ logger.error('Failed to dispatch action:', error)
throw error
}
}
@@ -149,7 +152,7 @@ export class ReduxService extends EventEmitter {
const newValue = await this.select(selector)
callback(newValue)
} catch (error) {
- console.error('Error in subscription handler:', error)
+ logger.error('Error in subscription handler:', error)
}
}
@@ -171,7 +174,7 @@ export class ReduxService extends EventEmitter {
window.store.getState()
`)
} catch (error) {
- console.error('Failed to get state:', error)
+ logger.error('Failed to get state:', error)
throw error
}
}
@@ -191,7 +194,7 @@ export const reduxService = new ReduxService()
try {
// 读取状态
const settings = await reduxService.select('state.settings')
- Logger.log('settings', settings)
+ logger.log('settings', settings)
// 派发 action
await reduxService.dispatch({
@@ -201,7 +204,7 @@ export const reduxService = new ReduxService()
// 订阅状态变化
const unsubscribe = await reduxService.subscribe('state.settings.apiKey', (newValue) => {
- Logger.log('API key changed:', newValue)
+ logger.log('API key changed:', newValue)
})
// 批量执行 actions
@@ -212,16 +215,16 @@ export const reduxService = new ReduxService()
// 同步方法虽然可能不是最新的数据,但响应更快
const apiKey = reduxService.selectSync('state.settings.apiKey')
- Logger.log('apiKey', apiKey)
+ logger.log('apiKey', apiKey)
// 处理保证是最新的数据
const apiKey1 = await reduxService.select('state.settings.apiKey')
- Logger.log('apiKey1', apiKey1)
+ logger.log('apiKey1', apiKey1)
// 取消订阅
unsubscribe()
} catch (error) {
- Logger.error('Error:', error)
+ logger.error('Error:', error)
}
}
*/
diff --git a/src/main/services/S3Storage.ts b/src/main/services/S3Storage.ts
index 0b45bb0387..6ce9cb6e23 100644
--- a/src/main/services/S3Storage.ts
+++ b/src/main/services/S3Storage.ts
@@ -6,11 +6,13 @@ import {
PutObjectCommand,
S3Client
} from '@aws-sdk/client-s3'
+import { loggerService } from '@logger'
import type { S3Config } from '@types'
-import Logger from 'electron-log'
import * as net from 'net'
import { Readable } from 'stream'
+const logger = loggerService.withContext('S3Storage')
+
/**
* 将可读流转换为 Buffer
*/
@@ -50,7 +52,7 @@ export default class S3Storage {
const isInWhiteList = VIRTUAL_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix))
return !isInWhiteList
} catch (e) {
- Logger.warn('[S3Storage] Failed to parse endpoint, fallback to Path-Style:', endpoint, e)
+ logger.warn('[S3Storage] Failed to parse endpoint, fallback to Path-Style:', endpoint, e)
return true
}
})()
@@ -96,7 +98,7 @@ export default class S3Storage {
})
)
} catch (error) {
- Logger.error('[S3Storage] Error putting object:', error)
+ logger.error('[S3Storage] Error putting object:', error)
throw error
}
}
@@ -109,7 +111,7 @@ export default class S3Storage {
}
return await streamToBuffer(res.Body as Readable)
} catch (error) {
- Logger.error('[S3Storage] Error getting object:', error)
+ logger.error('[S3Storage] Error getting object:', error)
throw error
}
}
@@ -126,7 +128,7 @@ export default class S3Storage {
}
}
} catch (error) {
- Logger.error('[S3Storage] Error deleting object:', error)
+ logger.error('[S3Storage] Error deleting object:', error)
throw error
}
}
@@ -163,7 +165,7 @@ export default class S3Storage {
return files
} catch (error) {
- Logger.error('[S3Storage] Error listing objects:', error)
+ logger.error('[S3Storage] Error listing objects:', error)
throw error
}
}
@@ -176,7 +178,7 @@ export default class S3Storage {
await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }))
return true
} catch (error) {
- Logger.error('[S3Storage] Error checking connection:', error)
+ logger.error('[S3Storage] Error checking connection:', error)
throw error
}
}
diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts
index 21520a3735..f20dc067d2 100644
--- a/src/main/services/SelectionService.ts
+++ b/src/main/services/SelectionService.ts
@@ -1,8 +1,8 @@
+import { loggerService } from '@logger'
import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig'
import { isDev, isMac, isWin } from '@main/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { app, BrowserWindow, ipcMain, screen, systemPreferences } from 'electron'
-import Logger from 'electron-log'
import { join } from 'path'
import type {
KeyboardEventData,
@@ -16,6 +16,8 @@ import type { ActionItem } from '../../renderer/src/types/selectionTypes'
import { ConfigKeys, configManager } from './ConfigManager'
import storeSyncService from './StoreSyncService'
+const logger = loggerService.withContext('SelectionService')
+
const isSupportedOS = isWin || isMac
let SelectionHook: SelectionHookConstructor | null = null
@@ -25,7 +27,7 @@ try {
SelectionHook = require('selection-hook')
}
} catch (error) {
- Logger.error('Failed to load selection-hook:', error)
+ logger.error('Failed to load selection-hook:', error)
}
// Type definitions
@@ -1504,12 +1506,12 @@ export class SelectionService {
private logInfo(message: string, forceShow: boolean = false): void {
if (isDev || forceShow) {
- Logger.info('[SelectionService] Info: ', message)
+ logger.info(message)
}
}
private logError(...args: [...string[], Error]): void {
- Logger.error('[SelectionService] Error: ', ...args)
+ logger.error('[SelectionService] Error: ', ...args)
}
}
@@ -1525,7 +1527,7 @@ export function initSelectionService(): boolean {
//avoid closure
const ss = SelectionService.getInstance()
if (!ss) {
- Logger.error('SelectionService not initialized: instance is null')
+ logger.error('SelectionService not initialized: instance is null')
return
}
@@ -1540,7 +1542,7 @@ export function initSelectionService(): boolean {
const ss = SelectionService.getInstance()
if (!ss) {
- Logger.error('SelectionService not initialized: instance is null')
+ logger.error('SelectionService not initialized: instance is null')
return false
}
diff --git a/src/main/services/ShortcutService.ts b/src/main/services/ShortcutService.ts
index 92544e17c0..12f786d797 100644
--- a/src/main/services/ShortcutService.ts
+++ b/src/main/services/ShortcutService.ts
@@ -1,12 +1,14 @@
+import { loggerService } from '@logger'
import { handleZoomFactor } from '@main/utils/zoom'
import { Shortcut } from '@types'
import { BrowserWindow, globalShortcut } from 'electron'
-import Logger from 'electron-log'
import { configManager } from './ConfigManager'
import selectionService from './SelectionService'
import { windowService } from './WindowService'
+const logger = loggerService.withContext('ShortcutService')
+
let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null
@@ -222,7 +224,7 @@ export function registerShortcuts(window: BrowserWindow) {
globalShortcut.register(accelerator, () => handler(window))
} catch (error) {
- Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
+ logger.warn(`Failed to register shortcut ${shortcut.key}`)
}
})
}
@@ -257,7 +259,7 @@ export function registerShortcuts(window: BrowserWindow) {
handler && globalShortcut.register(accelerator, () => handler(window))
}
} catch (error) {
- Logger.error('[ShortcutService] Failed to unregister shortcuts')
+ logger.warn('Failed to unregister shortcuts')
}
}
@@ -290,6 +292,6 @@ export function unregisterAllShortcuts() {
windowOnHandlers.clear()
globalShortcut.unregisterAll()
} catch (error) {
- Logger.error('[ShortcutService] Failed to unregister all shortcuts')
+ logger.warn('Failed to unregister all shortcuts')
}
}
diff --git a/src/main/services/WebDav.ts b/src/main/services/WebDav.ts
index 76996140e0..f055d16fbb 100644
--- a/src/main/services/WebDav.ts
+++ b/src/main/services/WebDav.ts
@@ -1,5 +1,5 @@
+import { loggerService } from '@logger'
import { WebDavConfig } from '@types'
-import Logger from 'electron-log'
import https from 'https'
import path from 'path'
import Stream from 'stream'
@@ -11,6 +11,9 @@ import {
PutFileContentsOptions,
WebDAVClient
} from 'webdav'
+
+const logger = loggerService.withContext('WebDav')
+
export default class WebDav {
public instance: WebDAVClient | undefined
private webdavPath: string
@@ -50,7 +53,7 @@ export default class WebDav {
})
}
} catch (error) {
- Logger.error('[WebDAV] Error creating directory on WebDAV:', error)
+ logger.error('Error creating directory on WebDAV:', error)
throw error
}
@@ -59,7 +62,7 @@ export default class WebDav {
try {
return await this.instance.putFileContents(remoteFilePath, data, options)
} catch (error) {
- Logger.error('[WebDAV] Error putting file contents on WebDAV:', error)
+ logger.error('Error putting file contents on WebDAV:', error)
throw error
}
}
@@ -74,7 +77,7 @@ export default class WebDav {
try {
return await this.instance.getFileContents(remoteFilePath, options)
} catch (error) {
- Logger.error('[WebDAV] Error getting file contents on WebDAV:', error)
+ logger.error('Error getting file contents on WebDAV:', error)
throw error
}
}
@@ -87,7 +90,7 @@ export default class WebDav {
try {
return await this.instance.getDirectoryContents(this.webdavPath)
} catch (error) {
- Logger.error('[WebDAV] Error getting directory contents on WebDAV:', error)
+ logger.error('Error getting directory contents on WebDAV:', error)
throw error
}
}
@@ -100,7 +103,7 @@ export default class WebDav {
try {
return await this.instance.exists('/')
} catch (error) {
- Logger.error('[WebDAV] Error checking connection:', error)
+ logger.error('Error checking connection:', error)
throw error
}
}
@@ -113,7 +116,7 @@ export default class WebDav {
try {
return await this.instance.createDirectory(path, options)
} catch (error) {
- Logger.error('[WebDAV] Error creating directory on WebDAV:', error)
+ logger.error('Error creating directory on WebDAV:', error)
throw error
}
}
@@ -128,7 +131,7 @@ export default class WebDav {
try {
return await this.instance.deleteFile(remoteFilePath)
} catch (error) {
- Logger.error('[WebDAV] Error deleting file on WebDAV:', error)
+ logger.error('Error deleting file on WebDAV:', error)
throw error
}
}
diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts
index fd2a3c9c84..2b81bc8050 100644
--- a/src/main/services/WindowService.ts
+++ b/src/main/services/WindowService.ts
@@ -2,11 +2,11 @@
import './ThemeService'
import { is } from '@electron-toolkit/utils'
+import { loggerService } from '@logger'
import { isDev, isLinux, isMac, isWin } from '@main/constant'
import { getFilesDir } from '@main/utils/file'
import { IpcChannel } from '@shared/IpcChannel'
import { app, BrowserWindow, nativeTheme, screen, shell } from 'electron'
-import Logger from 'electron-log'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
@@ -19,6 +19,9 @@ import { initSessionUserAgent } from './WebviewService'
const DEFAULT_MINIWINDOW_WIDTH = 550
const DEFAULT_MINIWINDOW_HEIGHT = 400
+// const logger = loggerService.withContext('WindowService')
+const logger = loggerService.withContext('WindowService')
+
export class WindowService {
private static instance: WindowService | null = null
private mainWindow: BrowserWindow | null = null
@@ -118,14 +121,14 @@ export class WindowService {
const spellCheckLanguages = configManager.get('spellCheckLanguages', []) as string[]
spellCheckLanguages.length > 0 && mainWindow.webContents.session.setSpellCheckerLanguages(spellCheckLanguages)
} catch (error) {
- Logger.error('Failed to set spell check languages:', error as Error)
+ logger.error('Failed to set spell check languages:', error as Error)
}
}
}
private setupMainWindowMonitor(mainWindow: BrowserWindow) {
mainWindow.webContents.on('render-process-gone', (_, details) => {
- Logger.error(`Renderer process crashed with: ${JSON.stringify(details)}`)
+ logger.error(`Renderer process crashed with: ${JSON.stringify(details)}`)
const currentTime = Date.now()
const lastCrashTime = this.lastRendererProcessCrashTime
this.lastRendererProcessCrashTime = currentTime
@@ -272,7 +275,7 @@ export class WindowService {
const fileName = url.replace('http://file/', '')
const storageDir = getFilesDir()
const filePath = storageDir + '/' + fileName
- shell.openPath(filePath).catch((err) => Logger.error('Failed to open file:', err))
+ shell.openPath(filePath).catch((err) => logger.error('Failed to open file:', err))
} else {
shell.openExternal(details.url)
}
@@ -625,7 +628,7 @@ export class WindowService {
}, 100)
}
} catch (error) {
- Logger.error('Failed to quote to main window:', error as Error)
+ logger.error('Failed to quote to main window:', error as Error)
}
}
}
diff --git a/src/main/services/mcp/oauth/callback.ts b/src/main/services/mcp/oauth/callback.ts
index db70827d00..71a4460fa2 100644
--- a/src/main/services/mcp/oauth/callback.ts
+++ b/src/main/services/mcp/oauth/callback.ts
@@ -1,10 +1,12 @@
-import Logger from 'electron-log'
+import { loggerService } from '@logger'
import EventEmitter from 'events'
import http from 'http'
import { URL } from 'url'
import { OAuthCallbackServerOptions } from './types'
+const logger = loggerService.withContext('MCP:OAuthCallbackServer')
+
export class CallBackServer {
private server: Promise
private events: EventEmitter
@@ -28,7 +30,7 @@ export class CallBackServer {
this.events.emit('auth-code-received', code)
}
} catch (error) {
- Logger.error('Error processing OAuth callback:', error)
+ logger.error('Error processing OAuth callback:', error)
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end('Internal Server Error')
}
@@ -41,12 +43,12 @@ export class CallBackServer {
// Handle server errors
server.on('error', (error) => {
- Logger.error('OAuth callback server error:', error)
+ logger.error('OAuth callback server error:', error)
})
return new Promise((resolve, reject) => {
server.listen(port, () => {
- Logger.info(`OAuth callback server listening on port ${port}`)
+ logger.info(`OAuth callback server listening on port ${port}`)
resolve(server)
})
diff --git a/src/main/services/mcp/oauth/provider.ts b/src/main/services/mcp/oauth/provider.ts
index a2a47fc15e..037ba70b75 100644
--- a/src/main/services/mcp/oauth/provider.ts
+++ b/src/main/services/mcp/oauth/provider.ts
@@ -1,14 +1,16 @@
import path from 'node:path'
+import { loggerService } from '@logger'
import { getConfigDir } from '@main/utils/file'
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth'
import { OAuthClientInformation, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth'
-import Logger from 'electron-log'
import open from 'open'
import { JsonFileStorage } from './storage'
import { OAuthProviderOptions } from './types'
+const logger = loggerService.withContext('MCP:OAuthClientProvider')
+
export class McpOAuthClientProvider implements OAuthClientProvider {
private storage: JsonFileStorage
public readonly config: Required
@@ -61,9 +63,9 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
try {
// Open the browser to the authorization URL
await open(authorizationUrl.toString())
- Logger.info('Browser opened automatically.')
+ logger.debug('Browser opened automatically.')
} catch (error) {
- Logger.error('Could not open browser automatically.')
+ logger.error('Could not open browser automatically.')
throw error // Let caller handle the error
}
}
diff --git a/src/main/services/mcp/oauth/storage.ts b/src/main/services/mcp/oauth/storage.ts
index 349fcf8bf1..95223119d2 100644
--- a/src/main/services/mcp/oauth/storage.ts
+++ b/src/main/services/mcp/oauth/storage.ts
@@ -1,14 +1,16 @@
+import { loggerService } from '@logger'
import {
OAuthClientInformation,
OAuthClientInformationFull,
OAuthTokens
} from '@modelcontextprotocol/sdk/shared/auth.js'
-import Logger from 'electron-log'
import fs from 'fs/promises'
import path from 'path'
import { IOAuthStorage, OAuthStorageData, OAuthStorageSchema } from './types'
+const logger = loggerService.withContext('MCP:OAuthStorage')
+
export class JsonFileStorage implements IOAuthStorage {
private readonly filePath: string
private cache: OAuthStorageData | null = null
@@ -38,7 +40,7 @@ export class JsonFileStorage implements IOAuthStorage {
await this.writeStorage(initial)
return initial
}
- Logger.error('Error reading OAuth storage:', error)
+ logger.error('Error reading OAuth storage:', error)
throw new Error(`Failed to read OAuth storage: ${error instanceof Error ? error.message : String(error)}`)
}
}
@@ -59,7 +61,7 @@ export class JsonFileStorage implements IOAuthStorage {
// Update cache
this.cache = data
} catch (error) {
- Logger.error('Error writing OAuth storage:', error)
+ logger.error('Error writing OAuth storage:', error)
throw new Error(`Failed to write OAuth storage: ${error instanceof Error ? error.message : String(error)}`)
}
}
@@ -112,7 +114,7 @@ export class JsonFileStorage implements IOAuthStorage {
this.cache = null
} catch (error) {
if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
- Logger.error('Error clearing OAuth storage:', error)
+ logger.error('Error clearing OAuth storage:', error)
throw new Error(`Failed to clear OAuth storage: ${error instanceof Error ? error.message : String(error)}`)
}
}
diff --git a/src/main/services/mcp/shell-env.ts b/src/main/services/mcp/shell-env.ts
index a4128b3651..e5de39c2e4 100644
--- a/src/main/services/mcp/shell-env.ts
+++ b/src/main/services/mcp/shell-env.ts
@@ -1,7 +1,9 @@
+import { loggerService } from '@logger'
import { spawn } from 'child_process'
-import Logger from 'electron-log'
import os from 'os'
+const logger = loggerService.withContext('ShellEnv')
+
/**
* Spawns a login shell in the user's home directory to capture its environment variables.
* @returns {Promise