refactor(Constants): 优化一些常量和枚举值 (#3773)

* refactor(main): 使用枚举管理 IPC 通道

- 新增 IpcChannel 枚举,用于统一管理所有的 IPC 通道
- 修改相关代码,使用 IpcChannel 枚举替代硬编码的字符串通道名称
- 此改动有助于提高代码的可维护性和可读性,避免因通道名称变更导致的错误

* refactor(ipc): 将字符串通道名称替换为 IpcChannel 枚举

- 在多个文件中将硬编码的字符串通道名称替换为 IpcChannel 枚举值
- 更新了相关文件的导入,增加了对 IpcChannel 的引用
- 通过使用枚举来管理 IPC 通道名称,提高了代码的可维护性和可读性

* refactor(ipc): 调整 IPC 通道枚举和预加载脚本

- 移除了 IpcChannel 枚举中的未使用注释
- 更新了预加载脚本中 IpcChannel 的导入路径

* refactor(ipc): 更新 IpcChannel导入路径

- 将 IpcChannel 的导入路径从 @main/enum/IpcChannel 修改为 @shared/IpcChannel
- 此修改涉及多个文件,包括 AppUpdater、BackupManager、EditMcpJsonPopup 等
- 同时移除了 tsconfig.web.json 中对 src/main/**/* 的引用

* refactor(ipc): 添加 ReduxStoreReady 事件并更新事件监听

- 在 IpcChannel 枚举中添加 ReduxStoreReady 事件
- 更新 ReduxService 中的事件监听,使用新的枚举值

* refactor(main): 重构 ReduxService 中的状态变化事件处理

- 将状态变化事件名称定义为常量 STATUS_CHANGE_EVENT
- 更新事件监听和触发使用新的常量
- 优化了代码结构,提高了可维护性

* refactor(i18n): 优化国际化配置和语言选择逻辑

- 在多个文件中引入 defaultLanguage 常量,统一默认语言设置
- 调整 i18n 初始化和语言变更逻辑,使用新配置
- 更新相关组件和 Hook 中的语言选择逻辑

* refactor(ConfigManager): 重构配置管理器

- 添加 ConfigKeys 枚举,用于统一配置项的键名
- 引入 defaultLanguage,作为默认语言设置
- 重构 get 和 set 方法,使用 ConfigKeys 枚举作为键名
- 优化类型定义和方法签名,提高代码可读性和可维护性

* refactor(ConfigManager): 重命名配置键 ZoomFactor

将配置键 zoomFactor 重命名为 ZoomFactor,以符合命名规范。
更新了相关方法和属性以反映这一变更。

* refactor(shared): 重构常量定义并优化文件大小格式化逻辑

- 在 constant.ts 中添加 KB、MB、GB 常量定义
- 将 defaultLanguage 移至 constant.ts
- 更新 ConfigManager、useAppInit、i18n、GeneralSettings 等文件中的导入路径
- 优化 formatFileSize 函数,使用新定义的常量

* refactor(FileSize): 使用 GB/MB/KB 等常量处理文件大小计算

* refactor(ipc): 将字符串通道名称替换为 IpcChannel 枚举

- 在多个文件中将硬编码的字符串通道名称替换为 IpcChannel 枚举值
- 更新了相关文件的导入,增加了对 IpcChannel 的引用
- 通过使用枚举来管理 IPC 通道名称,提高了代码的可维护性和可读性

* refactor(ipc): 更新 IpcChannel导入路径

- 将 IpcChannel 的导入路径从 @main/enum/IpcChannel 修改为 @shared/IpcChannel
- 此修改涉及多个文件,包括 AppUpdater、BackupManager、EditMcpJsonPopup 等
- 同时移除了 tsconfig.web.json 中对 src/main/**/* 的引用

* refactor(i18n): 优化国际化配置和语言选择逻辑

- 在多个文件中引入 defaultLanguage 常量,统一默认语言设置
- 调整 i18n 初始化和语言变更逻辑,使用新配置
- 更新相关组件和 Hook 中的语言选择逻辑

* refactor(shared): 重构常量定义并优化文件大小格式化逻辑

- 在 constant.ts 中添加 KB、MB、GB 常量定义
- 将 defaultLanguage 移至 constant.ts
- 更新 ConfigManager、useAppInit、i18n、GeneralSettings 等文件中的导入路径
- 优化 formatFileSize 函数,使用新定义的常量

* refactor: 移除重复的导入语句

- 在 HomeWindow.tsx 和 useAppInit.ts 文件中移除了重复的 defaultLanguage导入语句
- 这个改动简化了代码结构,提高了代码的可读性和维护性
This commit is contained in:
Hamm 2025-04-04 19:07:23 +08:00 committed by GitHub
parent ef8250ab72
commit 3290ac4b1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 518 additions and 303 deletions

View File

@ -0,0 +1,150 @@
export enum IpcChannel {
App_ClearCache = 'app:clear-cache',
App_SetLaunchOnBoot = 'app:set-launch-on-boot',
App_SetLanguage = 'app:set-language',
App_ShowUpdateDialog = 'app:show-update-dialog',
App_CheckForUpdate = 'app:check-for-update',
App_Reload = 'app:reload',
App_Info = 'app:info',
App_Proxy = 'app:proxy',
App_SetLaunchToTray = 'app:set-launch-to-tray',
App_SetTray = 'app:set-tray',
App_SetTrayOnClose = 'app:set-tray-on-close',
App_RestartTray = 'app:restart-tray',
App_SetTheme = 'app:set-theme',
App_IsBinaryExist = 'app:is-binary-exist',
App_GetBinaryPath = 'app:get-binary-path',
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
// Open
Open_Path = 'open:path',
Open_Website = 'open:website',
Minapp = 'minapp',
Config_Set = 'config:set',
Config_Get = 'config:get',
MiniWindow_Show = 'miniwindow:show',
MiniWindow_Hide = 'miniwindow:hide',
MiniWindow_Close = 'miniwindow:close',
MiniWindow_Toggle = 'miniwindow:toggle',
MiniWindow_SetPin = 'miniwindow:set-pin',
// Mcp
Mcp_RemoveServer = 'mcp:remove-server',
Mcp_RestartServer = 'mcp:restart-server',
Mcp_StopServer = 'mcp:stop-server',
Mcp_ListTools = 'mcp:list-tools',
Mcp_CallTool = 'mcp:call-tool',
Mcp_GetInstallInfo = 'mcp:get-install-info',
Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated',
//copilot
Copilot_GetAuthMessage = 'copilot:get-auth-message',
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
Copilot_SaveCopilotToken = 'copilot:save-copilot-token',
Copilot_GetToken = 'copilot:get-token',
Copilot_Logout = 'copilot:logout',
Copilot_GetUser = 'copilot:get-user',
// obsidian
Obsidian_GetVaults = 'obsidian:get-vaults',
Obsidian_GetFiles = 'obsidian:get-files',
// nutstore
Nutstore_GetSsoUrl = 'nutstore:get-sso-url',
Nutstore_DecryptToken = 'nutstore:decrypt-token',
Nutstore_GetDirectoryContents = 'nutstore:get-directory-contents',
//aes
Aes_Encrypt = 'aes:encrypt',
Aes_Decrypt = 'aes:decrypt',
Gemini_UploadFile = 'gemini:upload-file',
Gemini_Base64File = 'gemini:base64-file',
Gemini_RetrieveFile = 'gemini:retrieve-file',
Gemini_ListFiles = 'gemini:list-files',
Gemini_DeleteFile = 'gemini:delete-file',
Windows_ResetMinimumSize = 'window:reset-minimum-size',
Windows_SetMinimumSize = 'window:set-minimum-size',
SelectionMenu_Action = 'selection-menu:action',
KnowledgeBase_Create = 'knowledge-base:create',
KnowledgeBase_Reset = 'knowledge-base:reset',
KnowledgeBase_Delete = 'knowledge-base:delete',
KnowledgeBase_Add = 'knowledge-base:add',
KnowledgeBase_Remove = 'knowledge-base:remove',
KnowledgeBase_Search = 'knowledge-base:search',
KnowledgeBase_Rerank = 'knowledge-base:rerank',
//file
File_Open = 'file:open',
File_OpenPath = 'file:openPath',
File_Save = 'file:save',
File_Select = 'file:select',
File_Upload = 'file:upload',
File_Clear = 'file:clear',
File_Read = 'file:read',
File_Delete = 'file:delete',
File_Get = 'file:get',
File_SelectFolder = 'file:selectFolder',
File_Create = 'file:create',
File_Write = 'file:write',
File_SaveImage = 'file:saveImage',
File_Base64Image = 'file:base64Image',
File_Download = 'file:download',
File_Copy = 'file:copy',
File_BinaryFile = 'file:binaryFile',
Fs_Read = 'fs:read',
Export_Word = 'export:word',
Shortcuts_Update = 'shortcuts:update',
// backup
Backup_Backup = 'backup:backup',
Backup_Restore = 'backup:restore',
Backup_BackupToWebdav = 'backup:backupToWebdav',
Backup_RestoreFromWebdav = 'backup:restoreFromWebdav',
Backup_ListWebdavFiles = 'backup:listWebdavFiles',
Backup_CheckConnection = 'backup:checkConnection',
Backup_CreateDirectory = 'backup:createDirectory',
// zip
Zip_Compress = 'zip:compress',
Zip_Decompress = 'zip:decompress',
// system
System_GetDeviceType = 'system:getDeviceType',
// events
SelectionAction = 'selection-action',
BackupProgress = 'backup-progress',
ThemeChange = 'theme:change',
UpdateDownloadedCancelled = 'update-downloaded-cancelled',
RestoreProgress = 'restore-progress',
UpdateError = 'update-error',
UpdateAvailable = 'update-available',
UpdateNotAvailable = 'update-not-available',
DownloadProgress = 'download-progress',
UpdateDownloaded = 'update-downloaded',
DownloadUpdate = 'download-update',
DirectoryProcessingPercent = 'directory-processing-percent',
FullscreenStatusChanged = 'fullscreen-status-changed',
HideMiniWindow = 'hide-mini-window',
ShowMiniWindow = 'show-mini-window',
MiniWindowReload = 'miniwindow-reload',
ReduxStateChange = 'redux-state-change',
ReduxStoreReady = 'redux-store-ready',
}

View File

@ -157,3 +157,8 @@ export const ZOOM_SHORTCUTS = [
system: true system: true
} }
] ]
export const KB = 1024
export const MB = 1024 * KB
export const GB = 1024 * MB
export const defaultLanguage = 'en-US'

View File

@ -9,6 +9,7 @@ import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } fro
import { registerShortcuts } from './services/ShortcutService' import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService' import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
import { IpcChannel } from '@shared/IpcChannel'
// Check for single instance lock // Check for single instance lock
if (!app.requestSingleInstanceLock()) { if (!app.requestSingleInstanceLock()) {
@ -52,7 +53,7 @@ if (!app.requestSingleInstanceLock()) {
.then((name) => console.log(`Added Extension: ${name}`)) .then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err)) .catch((err) => console.log('An error occurred: ', err))
} }
ipcMain.handle('system:getDeviceType', () => { ipcMain.handle(IpcChannel.System_GetDeviceType, () => {
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux' return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
}) })
}) })

View File

@ -27,6 +27,7 @@ import { getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes' import { decrypt, encrypt } from './utils/aes'
import { getFilesDir } from './utils/file' import { getFilesDir } from './utils/file'
import { compress, decompress } from './utils/zip' import { compress, decompress } from './utils/zip'
import { IpcChannel } from '@shared/IpcChannel'
const fileManager = new FileStorage() const fileManager = new FileStorage()
const backupManager = new BackupManager() const backupManager = new BackupManager()
@ -36,7 +37,7 @@ const obsidianVaultService = new ObsidianVaultService()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater(mainWindow) const appUpdater = new AppUpdater(mainWindow)
ipcMain.handle('app:info', () => ({ ipcMain.handle(IpcChannel.App_Info, () => ({
version: app.getVersion(), version: app.getVersion(),
isPackaged: app.isPackaged, isPackaged: app.isPackaged,
appPath: app.getAppPath(), appPath: app.getAppPath(),
@ -46,7 +47,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
logsPath: log.transports.file.getFile().path logsPath: log.transports.file.getFile().path
})) }))
ipcMain.handle('app:proxy', async (_, proxy: string) => { ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string) => {
let proxyConfig: ProxyConfig let proxyConfig: ProxyConfig
if (proxy === 'system') { if (proxy === 'system') {
@ -60,19 +61,19 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
await proxyManager.configureProxy(proxyConfig) await proxyManager.configureProxy(proxyConfig)
}) })
ipcMain.handle('app:reload', () => mainWindow.reload()) ipcMain.handle(IpcChannel.App_Reload, () => mainWindow.reload())
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url)) ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
// Update // Update
ipcMain.handle('app:show-update-dialog', () => appUpdater.showUpdateDialog(mainWindow)) ipcMain.handle(IpcChannel.App_ShowUpdateDialog, () => appUpdater.showUpdateDialog(mainWindow))
// language // language
ipcMain.handle('app:set-language', (_, language) => { ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
configManager.setLanguage(language) configManager.setLanguage(language)
}) })
// launch on boot // launch on boot
ipcMain.handle('app:set-launch-on-boot', (_, openAtLogin: boolean) => { ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, openAtLogin: boolean) => {
// Set login item settings for windows and mac // Set login item settings for windows and mac
// linux is not supported because it requires more file operations // linux is not supported because it requires more file operations
if (isWin || isMac) { if (isWin || isMac) {
@ -81,32 +82,32 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
// launch to tray // launch to tray
ipcMain.handle('app:set-launch-to-tray', (_, isActive: boolean) => { ipcMain.handle(IpcChannel.App_SetLaunchToTray, (_, isActive: boolean) => {
configManager.setLaunchToTray(isActive) configManager.setLaunchToTray(isActive)
}) })
// tray // tray
ipcMain.handle('app:set-tray', (_, isActive: boolean) => { ipcMain.handle(IpcChannel.App_SetTray, (_, isActive: boolean) => {
configManager.setTray(isActive) configManager.setTray(isActive)
}) })
// to tray on close // to tray on close
ipcMain.handle('app:set-tray-on-close', (_, isActive: boolean) => { ipcMain.handle(IpcChannel.App_SetTrayOnClose, (_, isActive: boolean) => {
configManager.setTrayOnClose(isActive) configManager.setTrayOnClose(isActive)
}) })
ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray()) ipcMain.handle(IpcChannel.App_RestartTray, () => TrayService.getInstance().restartTray())
ipcMain.handle('config:set', (_, key: string, value: any) => { ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any) => {
configManager.set(key, value) configManager.set(key, value)
}) })
ipcMain.handle('config:get', (_, key: string) => { ipcMain.handle(IpcChannel.Config_Get, (_, key: string) => {
return configManager.get(key) return configManager.get(key)
}) })
// theme // theme
ipcMain.handle('app:set-theme', (event, theme: ThemeMode) => { ipcMain.handle(IpcChannel.App_SetTheme, (event, theme: ThemeMode) => {
if (theme === configManager.getTheme()) return if (theme === configManager.getTheme()) return
configManager.setTheme(theme) configManager.setTheme(theme)
@ -117,7 +118,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// 向其他窗口广播主题变化 // 向其他窗口广播主题变化
windows.forEach((win) => { windows.forEach((win) => {
if (win.webContents.id !== senderWindowId) { if (win.webContents.id !== senderWindowId) {
win.webContents.send('theme:change', theme) win.webContents.send(IpcChannel.ThemeChange, theme)
} }
}) })
@ -126,7 +127,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
// clear cache // clear cache
ipcMain.handle('app:clear-cache', async () => { ipcMain.handle(IpcChannel.App_ClearCache, async () => {
const sessions = [session.defaultSession, session.fromPartition('persist:webview')] const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
try { try {
@ -148,7 +149,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
// check for update // check for update
ipcMain.handle('app:check-for-update', async () => { ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
const update = await appUpdater.autoUpdater.checkForUpdates() const update = await appUpdater.autoUpdater.checkForUpdates()
return { return {
currentVersion: appUpdater.autoUpdater.currentVersion, currentVersion: appUpdater.autoUpdater.currentVersion,
@ -157,42 +158,42 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
// zip // zip
ipcMain.handle('zip:compress', (_, text: string) => compress(text)) ipcMain.handle(IpcChannel.Zip_Compress, (_, text: string) => compress(text))
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text)) ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
// backup // backup
ipcMain.handle('backup:backup', backupManager.backup) ipcMain.handle(IpcChannel.Backup_Backup, backupManager.backup)
ipcMain.handle('backup:restore', backupManager.restore) ipcMain.handle(IpcChannel.Backup_Restore, backupManager.restore)
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav) ipcMain.handle(IpcChannel.Backup_BackupToWebdav, backupManager.backupToWebdav)
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav) ipcMain.handle(IpcChannel.Backup_RestoreFromWebdav, backupManager.restoreFromWebdav)
ipcMain.handle('backup:listWebdavFiles', backupManager.listWebdavFiles) ipcMain.handle(IpcChannel.Backup_ListWebdavFiles, backupManager.listWebdavFiles)
ipcMain.handle('backup:checkConnection', backupManager.checkConnection) ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection)
ipcMain.handle('backup:createDirectory', backupManager.createDirectory) ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory)
// file // file
ipcMain.handle('file:open', fileManager.open) ipcMain.handle(IpcChannel.File_Open, fileManager.open)
ipcMain.handle('file:openPath', fileManager.openPath) ipcMain.handle(IpcChannel.File_OpenPath, fileManager.openPath)
ipcMain.handle('file:save', fileManager.save) ipcMain.handle(IpcChannel.File_Save, fileManager.save)
ipcMain.handle('file:select', fileManager.selectFile) ipcMain.handle(IpcChannel.File_Select, fileManager.selectFile)
ipcMain.handle('file:upload', fileManager.uploadFile) ipcMain.handle(IpcChannel.File_Upload, fileManager.uploadFile)
ipcMain.handle('file:clear', fileManager.clear) ipcMain.handle(IpcChannel.File_Clear, fileManager.clear)
ipcMain.handle('file:read', fileManager.readFile) ipcMain.handle(IpcChannel.File_Read, fileManager.readFile)
ipcMain.handle('file:delete', fileManager.deleteFile) ipcMain.handle(IpcChannel.File_Delete, fileManager.deleteFile)
ipcMain.handle('file:get', fileManager.getFile) ipcMain.handle(IpcChannel.File_Get, fileManager.getFile)
ipcMain.handle('file:selectFolder', fileManager.selectFolder) ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder)
ipcMain.handle('file:create', fileManager.createTempFile) ipcMain.handle(IpcChannel.File_Create, fileManager.createTempFile)
ipcMain.handle('file:write', fileManager.writeFile) ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile)
ipcMain.handle('file:saveImage', fileManager.saveImage) ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
ipcMain.handle('file:base64Image', fileManager.base64Image) ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
ipcMain.handle('file:download', fileManager.downloadFile) ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle('file:copy', fileManager.copyFile) ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
ipcMain.handle('file:binaryFile', fileManager.binaryFile) ipcMain.handle(IpcChannel.File_BinaryFile, fileManager.binaryFile)
// fs // fs
ipcMain.handle('fs:read', FileService.readFile) ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile)
// minapp // minapp
ipcMain.handle('minapp', (_, args) => { ipcMain.handle(IpcChannel.Minapp, (_, args) => {
windowService.createMinappWindow({ windowService.createMinappWindow({
url: args.url, url: args.url,
parent: mainWindow, parent: mainWindow,
@ -204,15 +205,15 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
// export // export
ipcMain.handle('export:word', exportService.exportToWord) ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord)
// open path // open path
ipcMain.handle('open:path', async (_, path: string) => { ipcMain.handle(IpcChannel.Open_Path, async (_, path: string) => {
await shell.openPath(path) await shell.openPath(path)
}) })
// shortcuts // shortcuts
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => { ipcMain.handle(IpcChannel.Shortcuts_Update, (_, shortcuts: Shortcut[]) => {
configManager.setShortcuts(shortcuts) configManager.setShortcuts(shortcuts)
// Refresh shortcuts registration // Refresh shortcuts registration
if (mainWindow) { if (mainWindow) {
@ -222,20 +223,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
// knowledge base // knowledge base
ipcMain.handle('knowledge-base:create', KnowledgeService.create) ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create)
ipcMain.handle('knowledge-base:reset', KnowledgeService.reset) ipcMain.handle(IpcChannel.KnowledgeBase_Reset, KnowledgeService.reset)
ipcMain.handle('knowledge-base:delete', KnowledgeService.delete) ipcMain.handle(IpcChannel.KnowledgeBase_Delete, KnowledgeService.delete)
ipcMain.handle('knowledge-base:add', KnowledgeService.add) ipcMain.handle(IpcChannel.KnowledgeBase_Add, KnowledgeService.add)
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove) ipcMain.handle(IpcChannel.KnowledgeBase_Remove, KnowledgeService.remove)
ipcMain.handle('knowledge-base:search', KnowledgeService.search) ipcMain.handle(IpcChannel.KnowledgeBase_Search, KnowledgeService.search)
ipcMain.handle('knowledge-base:rerank', KnowledgeService.rerank) ipcMain.handle(IpcChannel.KnowledgeBase_Rerank, KnowledgeService.rerank)
// window // window
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => { ipcMain.handle(IpcChannel.Windows_SetMinimumSize, (_, width: number, height: number) => {
mainWindow?.setMinimumSize(width, height) mainWindow?.setMinimumSize(width, height)
}) })
ipcMain.handle('window:reset-minimum-size', () => { ipcMain.handle(IpcChannel.Windows_ResetMinimumSize, () => {
mainWindow?.setMinimumSize(1080, 600) mainWindow?.setMinimumSize(1080, 600)
const [width, height] = mainWindow?.getSize() ?? [1080, 600] const [width, height] = mainWindow?.getSize() ?? [1080, 600]
if (width < 1080) { if (width < 1080) {
@ -244,59 +245,69 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
// gemini // gemini
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile) ipcMain.handle(IpcChannel.Gemini_UploadFile, GeminiService.uploadFile)
ipcMain.handle('gemini:base64-file', GeminiService.base64File) ipcMain.handle(IpcChannel.Gemini_Base64File, GeminiService.base64File)
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile) ipcMain.handle(IpcChannel.Gemini_RetrieveFile, GeminiService.retrieveFile)
ipcMain.handle('gemini:list-files', GeminiService.listFiles) ipcMain.handle(IpcChannel.Gemini_ListFiles, GeminiService.listFiles)
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile) ipcMain.handle(IpcChannel.Gemini_DeleteFile, GeminiService.deleteFile)
// mini window // mini window
ipcMain.handle('miniwindow:show', () => windowService.showMiniWindow()) ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow()) ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow()) ipcMain.handle(IpcChannel.MiniWindow_Close, () => windowService.closeMiniWindow())
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow()) ipcMain.handle(IpcChannel.MiniWindow_Toggle, () => windowService.toggleMiniWindow())
ipcMain.handle('miniwindow:set-pin', (_, isPinned) => windowService.setPinMiniWindow(isPinned)) ipcMain.handle(IpcChannel.MiniWindow_SetPin, (_, isPinned) => windowService.setPinMiniWindow(isPinned))
// aes // aes
ipcMain.handle('aes:encrypt', (_, text: string, secretKey: string, iv: string) => encrypt(text, secretKey, iv)) ipcMain.handle(IpcChannel.Aes_Encrypt, (_, text: string, secretKey: string, iv: string) =>
ipcMain.handle('aes:decrypt', (_, encryptedData: string, iv: string, secretKey: string) => encrypt(text, secretKey, iv)
)
ipcMain.handle(IpcChannel.Aes_Decrypt, (_, encryptedData: string, iv: string, secretKey: string) =>
decrypt(encryptedData, iv, secretKey) decrypt(encryptedData, iv, secretKey)
) )
// Register MCP handlers // Register MCP handlers
ipcMain.handle('mcp:remove-server', mcpService.removeServer) ipcMain.handle(IpcChannel.Mcp_RemoveServer, mcpService.removeServer)
ipcMain.handle('mcp:restart-server', mcpService.restartServer) ipcMain.handle(IpcChannel.Mcp_RestartServer, mcpService.restartServer)
ipcMain.handle('mcp:stop-server', mcpService.stopServer) ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
ipcMain.handle('mcp:list-tools', mcpService.listTools) ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
ipcMain.handle('mcp:call-tool', mcpService.callTool) ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo) ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name)) ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name)) ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js')) ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js'))
ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js')) ipcMain.handle(IpcChannel.App_InstallBunBinary, () => runInstallScript('install-bun.js'))
//copilot //copilot
ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage) ipcMain.handle(IpcChannel.Copilot_GetAuthMessage, CopilotService.getAuthMessage)
ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken) ipcMain.handle(IpcChannel.Copilot_GetCopilotToken, CopilotService.getCopilotToken)
ipcMain.handle('copilot:save-copilot-token', CopilotService.saveCopilotToken) ipcMain.handle(IpcChannel.Copilot_SaveCopilotToken, CopilotService.saveCopilotToken)
ipcMain.handle('copilot:get-token', CopilotService.getToken) ipcMain.handle(IpcChannel.Copilot_GetToken, CopilotService.getToken)
ipcMain.handle('copilot:logout', CopilotService.logout) ipcMain.handle(IpcChannel.Copilot_Logout, CopilotService.logout)
ipcMain.handle('copilot:get-user', CopilotService.getUser) ipcMain.handle(IpcChannel.Copilot_GetUser, CopilotService.getUser)
// Obsidian service // Obsidian service
ipcMain.handle('obsidian:get-vaults', () => { ipcMain.handle(IpcChannel.Obsidian_GetVaults, () => {
return obsidianVaultService.getVaults() return obsidianVaultService.getVaults()
}) })
ipcMain.handle('obsidian:get-files', (_event, vaultName) => { ipcMain.handle(IpcChannel.Obsidian_GetFiles, (_event, vaultName) => {
return obsidianVaultService.getFilesByVaultName(vaultName) return obsidianVaultService.getFilesByVaultName(vaultName)
}) })
// nutstore // nutstore
ipcMain.handle('nutstore:get-sso-url', NutstoreService.getNutstoreSSOUrl) ipcMain.handle(IpcChannel.Nutstore_GetSsoUrl, NutstoreService.getNutstoreSSOUrl)
ipcMain.handle('nutstore:decrypt-token', (_, token: string) => NutstoreService.decryptToken(token)) ipcMain.handle(IpcChannel.Nutstore_DecryptToken, (_, token: string) => NutstoreService.decryptToken(token))
ipcMain.handle('nutstore:get-directory-contents', (_, token: string, path: string) => ipcMain.handle(IpcChannel.Nutstore_GetDirectoryContents, (_, token: string, path: string) =>
NutstoreService.getDirectoryContents(token, path) NutstoreService.getDirectoryContents(token, path)
) )
} }
// Listen for changes in MCP servers and notify renderer
mcpService.on('servers-updated', (servers) => {
mainWindow?.webContents.send(IpcChannel.Mcp_ServersUpdated, servers)
})
app.on('before-quit', () => mcpService.cleanup())

View File

@ -4,6 +4,7 @@ import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater' import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
import icon from '../../../build/icon.png?asset' import icon from '../../../build/icon.png?asset'
import { IpcChannel } from '@shared/IpcChannel'
export default class AppUpdater { export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater autoUpdater: _AppUpdater = autoUpdater
@ -24,27 +25,27 @@ export default class AppUpdater {
stack: error.stack, stack: error.stack,
time: new Date().toISOString() time: new Date().toISOString()
}) })
mainWindow.webContents.send('update-error', error) mainWindow.webContents.send(IpcChannel.UpdateError, error)
}) })
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => { autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
logger.info('检测到新版本', releaseInfo) logger.info('检测到新版本', releaseInfo)
mainWindow.webContents.send('update-available', releaseInfo) mainWindow.webContents.send(IpcChannel.UpdateAvailable, releaseInfo)
}) })
// 检测到不需要更新时 // 检测到不需要更新时
autoUpdater.on('update-not-available', () => { autoUpdater.on('update-not-available', () => {
mainWindow.webContents.send('update-not-available') mainWindow.webContents.send(IpcChannel.UpdateNotAvailable)
}) })
// 更新下载进度 // 更新下载进度
autoUpdater.on('download-progress', (progress) => { autoUpdater.on('download-progress', (progress) => {
mainWindow.webContents.send('download-progress', progress) mainWindow.webContents.send(IpcChannel.DownloadProgress, progress)
}) })
// 当需要更新的内容下载完成后 // 当需要更新的内容下载完成后
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => { autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
mainWindow.webContents.send('update-downloaded', releaseInfo) mainWindow.webContents.send(IpcChannel.UpdateDownloaded, releaseInfo)
this.releaseInfo = releaseInfo this.releaseInfo = releaseInfo
logger.info('下载完成', releaseInfo) logger.info('下载完成', releaseInfo)
}) })
@ -73,7 +74,7 @@ export default class AppUpdater {
app.isQuitting = true app.isQuitting = true
setImmediate(() => autoUpdater.quitAndInstall()) setImmediate(() => autoUpdater.quitAndInstall())
} else { } else {
mainWindow.webContents.send('update-downloaded-cancelled') mainWindow.webContents.send(IpcChannel.UpdateDownloadedCancelled)
} }
}) })
} }

View File

@ -9,6 +9,7 @@ import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
import WebDav from './WebDav' import WebDav from './WebDav'
import { windowService } from './WindowService' import { windowService } from './WindowService'
import { IpcChannel } from '@shared/IpcChannel'
class BackupManager { class BackupManager {
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp') private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
@ -79,7 +80,7 @@ class BackupManager {
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
const onProgress = (processData: { stage: string; progress: number; total: number }) => { const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send('backup-progress', processData) mainWindow?.webContents.send(IpcChannel.BackupProgress, processData)
Logger.log('[BackupManager] backup progress', processData) Logger.log('[BackupManager] backup progress', processData)
} }
@ -139,7 +140,7 @@ class BackupManager {
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
const onProgress = (processData: { stage: string; progress: number; total: number }) => { const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send('restore-progress', processData) mainWindow?.webContents.send(IpcChannel.RestoreProgress, processData)
Logger.log('[BackupManager] restore progress', processData) Logger.log('[BackupManager] restore progress', processData)
} }

View File

@ -1,10 +1,22 @@
import { ZOOM_SHORTCUTS } from '@shared/config/constant' import { defaultLanguage, ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types' import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron' import { app } from 'electron'
import Store from 'electron-store' import Store from 'electron-store'
import { locales } from '../utils/locales' import { locales } from '../utils/locales'
enum ConfigKeys {
Language = 'language',
Theme = 'theme',
LaunchToTray = 'launchToTray',
Tray = 'tray',
TrayOnClose = 'trayOnClose',
ZoomFactor = 'ZoomFactor',
Shortcuts = 'shortcuts',
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
EnableQuickAssistant = 'enableQuickAssistant'
}
export class ConfigManager { export class ConfigManager {
private store: Store private store: Store
private subscribers: Map<string, Array<(newValue: any) => void>> = new Map() private subscribers: Map<string, Array<(newValue: any) => void>> = new Map()
@ -14,54 +26,54 @@ export class ConfigManager {
} }
getLanguage(): LanguageVarious { getLanguage(): LanguageVarious {
const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : 'en-US' const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : defaultLanguage
return this.store.get('language', locale) as LanguageVarious return this.get(ConfigKeys.Language, locale) as LanguageVarious
} }
setLanguage(theme: LanguageVarious) { setLanguage(theme: LanguageVarious) {
this.store.set('language', theme) this.set(ConfigKeys.Language, theme)
} }
getTheme(): ThemeMode { getTheme(): ThemeMode {
return this.store.get('theme', ThemeMode.light) as ThemeMode return this.get(ConfigKeys.Theme, ThemeMode.light)
} }
setTheme(theme: ThemeMode) { setTheme(theme: ThemeMode) {
this.store.set('theme', theme) this.set(ConfigKeys.Theme, theme)
} }
getLaunchToTray(): boolean { getLaunchToTray(): boolean {
return !!this.store.get('launchToTray', false) return !!this.get(ConfigKeys.LaunchToTray, false)
} }
setLaunchToTray(value: boolean) { setLaunchToTray(value: boolean) {
this.store.set('launchToTray', value) this.set(ConfigKeys.LaunchToTray, value)
} }
getTray(): boolean { getTray(): boolean {
return !!this.store.get('tray', true) return !!this.get(ConfigKeys.Tray, true)
} }
setTray(value: boolean) { setTray(value: boolean) {
this.store.set('tray', value) this.set(ConfigKeys.Tray, value)
this.notifySubscribers('tray', value) this.notifySubscribers(ConfigKeys.Tray, value)
} }
getTrayOnClose(): boolean { getTrayOnClose(): boolean {
return !!this.store.get('trayOnClose', true) return !!this.get(ConfigKeys.TrayOnClose, true)
} }
setTrayOnClose(value: boolean) { setTrayOnClose(value: boolean) {
this.store.set('trayOnClose', value) this.set(ConfigKeys.TrayOnClose, value)
} }
getZoomFactor(): number { getZoomFactor(): number {
return this.store.get('zoomFactor', 1) as number return this.get<number>(ConfigKeys.ZoomFactor, 1)
} }
setZoomFactor(factor: number) { setZoomFactor(factor: number) {
this.store.set('zoomFactor', factor) this.set(ConfigKeys.ZoomFactor, factor)
this.notifySubscribers('zoomFactor', factor) this.notifySubscribers(ConfigKeys.ZoomFactor, factor)
} }
subscribe<T>(key: string, callback: (newValue: T) => void) { subscribe<T>(key: string, callback: (newValue: T) => void) {
@ -89,39 +101,39 @@ export class ConfigManager {
} }
getShortcuts() { getShortcuts() {
return this.store.get('shortcuts', ZOOM_SHORTCUTS) as Shortcut[] | [] return this.get(ConfigKeys.Shortcuts, ZOOM_SHORTCUTS) as Shortcut[] | []
} }
setShortcuts(shortcuts: Shortcut[]) { setShortcuts(shortcuts: Shortcut[]) {
this.store.set( this.set(
'shortcuts', ConfigKeys.Shortcuts,
shortcuts.filter((shortcut) => shortcut.system) shortcuts.filter((shortcut) => shortcut.system)
) )
this.notifySubscribers('shortcuts', shortcuts) this.notifySubscribers(ConfigKeys.Shortcuts, shortcuts)
} }
getClickTrayToShowQuickAssistant(): boolean { getClickTrayToShowQuickAssistant(): boolean {
return this.store.get('clickTrayToShowQuickAssistant', false) as boolean return this.get<boolean>(ConfigKeys.ClickTrayToShowQuickAssistant, false)
} }
setClickTrayToShowQuickAssistant(value: boolean) { setClickTrayToShowQuickAssistant(value: boolean) {
this.store.set('clickTrayToShowQuickAssistant', value) this.set(ConfigKeys.ClickTrayToShowQuickAssistant, value)
} }
getEnableQuickAssistant(): boolean { getEnableQuickAssistant(): boolean {
return this.store.get('enableQuickAssistant', false) as boolean return this.get(ConfigKeys.EnableQuickAssistant, false)
} }
setEnableQuickAssistant(value: boolean) { setEnableQuickAssistant(value: boolean) {
this.store.set('enableQuickAssistant', value) this.set(ConfigKeys.EnableQuickAssistant, value)
} }
set(key: string, value: any) { set(key: string, value: unknown) {
this.store.set(key, value) this.store.set(key, value)
} }
get(key: string) { get<T>(key: string, defaultValue?: T) {
return this.store.get(key) return this.store.get(key, defaultValue) as T
} }
} }

View File

@ -1,5 +1,5 @@
import { getFilesDir, getFileType, getTempDir } from '@main/utils/file' import { getFilesDir, getFileType, getTempDir } from '@main/utils/file'
import { documentExts, imageExts } from '@shared/config/constant' import { documentExts, imageExts, MB } from '@shared/config/constant'
import { FileType } from '@types' import { FileType } from '@types'
import * as crypto from 'crypto' import * as crypto from 'crypto'
import { import {
@ -122,7 +122,7 @@ class FileStorage {
private async compressImage(sourcePath: string, destPath: string): Promise<void> { private async compressImage(sourcePath: string, destPath: string): Promise<void> {
try { try {
const stats = fs.statSync(sourcePath) const stats = fs.statSync(sourcePath)
const fileSizeInMB = stats.size / (1024 * 1024) const fileSizeInMB = stats.size / MB
// 如果图片大于1MB才进行压缩 // 如果图片大于1MB才进行压缩
if (fileSizeInMB > 1) { if (fileSizeInMB > 1) {

View File

@ -31,6 +31,8 @@ import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
import { app } from 'electron' import { app } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { IpcChannel } from '@shared/IpcChannel'
import { MB } from '@shared/config/constant'
export interface KnowledgeBaseAddItemOptions { export interface KnowledgeBaseAddItemOptions {
base: KnowledgeBaseParams base: KnowledgeBaseParams
@ -91,7 +93,7 @@ class KnowledgeService {
private workload = 0 private workload = 0
private processingItemCount = 0 private processingItemCount = 0
private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map() private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map()
private static MAXIMUM_WORKLOAD = 1024 * 1024 * 80 private static MAXIMUM_WORKLOAD = 80 * MB
private static MAXIMUM_PROCESSING_ITEM_COUNT = 30 private static MAXIMUM_PROCESSING_ITEM_COUNT = 30
private static ERROR_LOADER_RETURN: LoaderReturn = { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' } private static ERROR_LOADER_RETURN: LoaderReturn = { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' }
@ -194,7 +196,7 @@ class KnowledgeService {
const sendDirectoryProcessingPercent = (totalFiles: number, processedFiles: number) => { const sendDirectoryProcessingPercent = (totalFiles: number, processedFiles: number) => {
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
mainWindow?.webContents.send('directory-processing-percent', { mainWindow?.webContents.send(IpcChannel.DirectoryProcessingPercent, {
itemId: item.id, itemId: item.id,
percent: (processedFiles / totalFiles) * 100 percent: (processedFiles / totalFiles) * 100
}) })
@ -270,7 +272,7 @@ class KnowledgeService {
return KnowledgeService.ERROR_LOADER_RETURN return KnowledgeService.ERROR_LOADER_RETURN
}) })
}, },
evaluateTaskWorkload: { workload: 1024 * 1024 * 2 } evaluateTaskWorkload: { workload: 2 * MB }
} }
], ],
loaderDoneReturn: null loaderDoneReturn: null
@ -309,7 +311,7 @@ class KnowledgeService {
Logger.error(err) Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN return KnowledgeService.ERROR_LOADER_RETURN
}), }),
evaluateTaskWorkload: { workload: 1024 * 1024 * 20 } evaluateTaskWorkload: { workload: 20 * MB }
} }
], ],
loaderDoneReturn: null loaderDoneReturn: null

View File

@ -2,6 +2,7 @@ import { ipcMain } from 'electron'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { windowService } from './WindowService' import { windowService } from './WindowService'
import { IpcChannel } from '@shared/IpcChannel'
type StoreValue = any type StoreValue = any
type Unsubscribe = () => void type Unsubscribe = () => void
@ -10,6 +11,8 @@ export class ReduxService extends EventEmitter {
private stateCache: any = {} private stateCache: any = {}
private isReady = false private isReady = false
private readonly STATUS_CHANGE_EVENT = 'statusChange'
constructor() { constructor() {
super() super()
this.setupIpcHandlers() this.setupIpcHandlers()
@ -17,15 +20,15 @@ export class ReduxService extends EventEmitter {
private setupIpcHandlers() { private setupIpcHandlers() {
// 监听 store 就绪事件 // 监听 store 就绪事件
ipcMain.handle('redux-store-ready', () => { ipcMain.handle(IpcChannel.ReduxStoreReady, () => {
this.isReady = true this.isReady = true
this.emit('ready') this.emit('ready')
}) })
// 监听 store 状态变化 // 监听 store 状态变化
ipcMain.on('redux-state-change', (_, newState) => { ipcMain.on(IpcChannel.ReduxStateChange, (_, newState) => {
this.stateCache = newState this.stateCache = newState
this.emit('stateChange', newState) this.emit(this.STATUS_CHANGE_EVENT, newState)
}) })
} }
@ -122,19 +125,23 @@ export class ReduxService extends EventEmitter {
await this.waitForStoreReady(mainWindow.webContents) await this.waitForStoreReady(mainWindow.webContents)
// 在渲染进程中设置监听 // 在渲染进程中设置监听
await mainWindow.webContents.executeJavaScript(` await mainWindow.webContents.executeJavaScript(
`
if (!window._storeSubscriptions) { if (!window._storeSubscriptions) {
window._storeSubscriptions = new Set(); window._storeSubscriptions = new Set();
// 设置全局状态变化监听 // 设置全局状态变化监听
const unsubscribe = window.store.subscribe(() => { const unsubscribe = window.store.subscribe(() => {
const state = window.store.getState(); const state = window.store.getState();
window.electron.ipcRenderer.send('redux-state-change', state); window.electron.ipcRenderer.send('` +
IpcChannel.ReduxStateChange +
`', state);
}); });
window._storeSubscriptions.add(unsubscribe); window._storeSubscriptions.add(unsubscribe);
} }
`) `
)
// 在主进程中处理回调 // 在主进程中处理回调
const handler = async () => { const handler = async () => {
@ -146,9 +153,9 @@ export class ReduxService extends EventEmitter {
} }
} }
this.on('stateChange', handler) this.on(this.STATUS_CHANGE_EVENT, handler)
return () => { return () => {
this.off('stateChange', handler) this.off(this.STATUS_CHANGE_EVENT, handler)
} }
} }
@ -180,7 +187,7 @@ export class ReduxService extends EventEmitter {
export const reduxService = new ReduxService() export const reduxService = new ReduxService()
/** example /** example
async function example() { async function example() {
try { try {
// 读取状态 // 读取状态
const settings = await reduxService.select('state.settings') const settings = await reduxService.select('state.settings')
@ -216,5 +223,5 @@ async function example() {
} catch (error) { } catch (error) {
console.error('Error:', error) console.error('Error:', error)
} }
} }
*/ */

View File

@ -10,6 +10,7 @@ import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config' import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { locales } from '../utils/locales' import { locales } from '../utils/locales'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
import { IpcChannel } from '@shared/IpcChannel'
export class WindowService { export class WindowService {
private static instance: WindowService | null = null private static instance: WindowService | null = null
@ -168,12 +169,12 @@ export class WindowService {
// 处理全屏相关事件 // 处理全屏相关事件
mainWindow.on('enter-full-screen', () => { mainWindow.on('enter-full-screen', () => {
this.wasFullScreen = true this.wasFullScreen = true
mainWindow.webContents.send('fullscreen-status-changed', true) mainWindow.webContents.send(IpcChannel.FullscreenStatusChanged, true)
}) })
mainWindow.on('leave-full-screen', () => { mainWindow.on('leave-full-screen', () => {
this.wasFullScreen = false this.wasFullScreen = false
mainWindow.webContents.send('fullscreen-status-changed', false) mainWindow.webContents.send(IpcChannel.FullscreenStatusChanged, false)
}) })
// set the zoom factor again when the window is going to resize // set the zoom factor again when the window is going to resize
@ -434,14 +435,14 @@ export class WindowService {
}) })
this.miniWindow.on('hide', () => { this.miniWindow.on('hide', () => {
this.miniWindow?.webContents.send('hide-mini-window') this.miniWindow?.webContents.send(IpcChannel.HideMiniWindow)
}) })
this.miniWindow.on('show', () => { this.miniWindow.on('show', () => {
this.miniWindow?.webContents.send('show-mini-window') this.miniWindow?.webContents.send(IpcChannel.ShowMiniWindow)
}) })
ipcMain.on('miniwindow-reload', () => { ipcMain.on(IpcChannel.MiniWindowReload, () => {
this.miniWindow?.reload() this.miniWindow?.reload()
}) })
@ -547,7 +548,7 @@ export class WindowService {
// 点击其他地方时隐藏窗口 // 点击其他地方时隐藏窗口
this.selectionMenuWindow.on('blur', () => { this.selectionMenuWindow.on('blur', () => {
this.selectionMenuWindow?.hide() this.selectionMenuWindow?.hide()
this.miniWindow?.webContents.send('selection-action', { this.miniWindow?.webContents.send(IpcChannel.SelectionAction, {
action: 'home', action: 'home',
selectedText: this.lastSelectedText selectedText: this.lastSelectedText
}) })
@ -565,12 +566,12 @@ export class WindowService {
private setupSelectionMenuEvents() { private setupSelectionMenuEvents() {
if (!this.selectionMenuWindow) return if (!this.selectionMenuWindow) return
ipcMain.removeHandler('selection-menu:action') ipcMain.removeHandler(IpcChannel.SelectionMenu_Action)
ipcMain.handle('selection-menu:action', (_, action) => { ipcMain.handle(IpcChannel.SelectionMenu_Action, (_, action) => {
this.selectionMenuWindow?.hide() this.selectionMenuWindow?.hide()
this.showMiniWindow() this.showMiniWindow()
setTimeout(() => { setTimeout(() => {
this.miniWindow?.webContents.send('selection-action', { this.miniWindow?.webContents.send(IpcChannel.SelectionAction, {
action, action,
selectedText: this.lastSelectedText selectedText: this.lastSelectedText
}) })

View File

@ -3,77 +3,78 @@ import { electronAPI } from '@electron-toolkit/preload'
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, WebDavConfig } from '@types' import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron' import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
import { CreateDirectoryOptions } from 'webdav' import { CreateDirectoryOptions } from 'webdav'
import { IpcChannel } from '@shared/IpcChannel'
// Custom APIs for renderer // Custom APIs for renderer
const api = { const api = {
getAppInfo: () => ipcRenderer.invoke('app:info'), getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info),
reload: () => ipcRenderer.invoke('app:reload'), reload: () => ipcRenderer.invoke(IpcChannel.App_Reload),
setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy), setProxy: (proxy: string) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy),
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'), checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
showUpdateDialog: () => ipcRenderer.invoke('app:show-update-dialog'), showUpdateDialog: () => ipcRenderer.invoke(IpcChannel.App_ShowUpdateDialog),
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang), setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
setLaunchOnBoot: (isActive: boolean) => ipcRenderer.invoke('app:set-launch-on-boot', isActive), setLaunchOnBoot: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchOnBoot, isActive),
setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke('app:set-launch-to-tray', isActive), setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive),
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive), setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke('app:set-tray-on-close', isActive), setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
restartTray: () => ipcRenderer.invoke('app:restart-tray'), restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme), setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url), openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
minApp: (url: string) => ipcRenderer.invoke('minapp', url), minApp: (url: string) => ipcRenderer.invoke(IpcChannel.Minapp, url),
clearCache: () => ipcRenderer.invoke('app:clear-cache'), clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
system: { system: {
getDeviceType: () => ipcRenderer.invoke('system:getDeviceType') getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType)
}, },
zip: { zip: {
compress: (text: string) => ipcRenderer.invoke('zip:compress', text), compress: (text: string) => ipcRenderer.invoke(IpcChannel.Zip_Compress, text),
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text) decompress: (text: Buffer) => ipcRenderer.invoke(IpcChannel.Zip_Decompress, text)
}, },
backup: { backup: {
backup: (fileName: string, data: string, destinationPath?: string) => backup: (fileName: string, data: string, destinationPath?: string) =>
ipcRenderer.invoke('backup:backup', fileName, data, destinationPath), ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath),
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath), restore: (backupPath: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, backupPath),
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig), ipcRenderer.invoke(IpcChannel.Backup_BackupToWebdav, data, webdavConfig),
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig), restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke(IpcChannel.Backup_RestoreFromWebdav, webdavConfig),
listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:listWebdavFiles', webdavConfig), listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke(IpcChannel.Backup_ListWebdavFiles, webdavConfig),
checkConnection: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:checkConnection', webdavConfig), checkConnection: (webdavConfig: WebDavConfig) => ipcRenderer.invoke(IpcChannel.Backup_CheckConnection, webdavConfig),
createDirectory: (webdavConfig: WebDavConfig, path: string, options?: CreateDirectoryOptions) => createDirectory: (webdavConfig: WebDavConfig, path: string, options?: CreateDirectoryOptions) =>
ipcRenderer.invoke('backup:createDirectory', webdavConfig, path, options) ipcRenderer.invoke(IpcChannel.Backup_CreateDirectory, webdavConfig, path, options)
}, },
file: { file: {
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options), select: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Select, options),
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath), upload: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Upload, filePath),
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId), delete: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Delete, fileId),
read: (fileId: string) => ipcRenderer.invoke('file:read', fileId), read: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Read, fileId),
clear: () => ipcRenderer.invoke('file:clear'), clear: () => ipcRenderer.invoke(IpcChannel.File_Clear),
get: (filePath: string) => ipcRenderer.invoke('file:get', filePath), get: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Get, filePath),
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName), create: (fileName: string) => ipcRenderer.invoke(IpcChannel.File_Create, fileName),
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data), write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke(IpcChannel.File_Write, filePath, data),
open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options), open: (options?: { decompress: boolean }) => ipcRenderer.invoke(IpcChannel.File_Open, options),
openPath: (path: string) => ipcRenderer.invoke('file:openPath', path), openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
save: (path: string, content: string, options?: { compress: boolean }) => save: (path: string, content: string, options?: { compress: boolean }) =>
ipcRenderer.invoke('file:save', path, content, options), ipcRenderer.invoke(IpcChannel.File_Save, path, content, options),
selectFolder: () => ipcRenderer.invoke('file:selectFolder'), selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data), saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId), base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
download: (url: string) => ipcRenderer.invoke('file:download', url), download: (url: string) => ipcRenderer.invoke(IpcChannel.File_Download, url),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath), copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
binaryFile: (fileId: string) => ipcRenderer.invoke('file:binaryFile', fileId) binaryFile: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryFile, fileId)
}, },
fs: { fs: {
read: (path: string) => ipcRenderer.invoke('fs:read', path) read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path)
}, },
export: { export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName) toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
}, },
openPath: (path: string) => ipcRenderer.invoke('open:path', path), openPath: (path: string) => ipcRenderer.invoke(IpcChannel.Open_Path, path),
shortcuts: { shortcuts: {
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts) update: (shortcuts: Shortcut[]) => ipcRenderer.invoke(IpcChannel.Shortcuts_Update, shortcuts)
}, },
knowledgeBase: { knowledgeBase: {
create: (base: KnowledgeBaseParams) => ipcRenderer.invoke('knowledge-base:create', base), create: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Create, base),
reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke('knowledge-base:reset', base), reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Reset, base),
delete: (id: string) => ipcRenderer.invoke('knowledge-base:delete', id), delete: (id: string) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Delete, id),
add: ({ add: ({
base, base,
item, item,
@ -82,71 +83,74 @@ const api = {
base: KnowledgeBaseParams base: KnowledgeBaseParams
item: KnowledgeItem item: KnowledgeItem
forceReload?: boolean forceReload?: boolean
}) => ipcRenderer.invoke('knowledge-base:add', { base, item, forceReload }), }) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Add, { base, item, forceReload }),
remove: ({ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }) => remove: ({ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, uniqueIds, base }), ipcRenderer.invoke(IpcChannel.KnowledgeBase_Remove, { uniqueId, uniqueIds, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:search', { search, base }), ipcRenderer.invoke(IpcChannel.KnowledgeBase_Search, { search, base }),
rerank: ({ search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] }) => rerank: ({ search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] }) =>
ipcRenderer.invoke('knowledge-base:rerank', { search, base, results }) ipcRenderer.invoke(IpcChannel.KnowledgeBase_Rerank, { search, base, results })
}, },
window: { window: {
setMinimumSize: (width: number, height: number) => ipcRenderer.invoke('window:set-minimum-size', width, height), setMinimumSize: (width: number, height: number) =>
resetMinimumSize: () => ipcRenderer.invoke('window:reset-minimum-size') ipcRenderer.invoke(IpcChannel.Windows_SetMinimumSize, width, height),
resetMinimumSize: () => ipcRenderer.invoke(IpcChannel.Windows_ResetMinimumSize)
}, },
gemini: { gemini: {
uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:upload-file', file, apiKey), uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_UploadFile, file, apiKey),
base64File: (file: FileType) => ipcRenderer.invoke('gemini:base64-file', file), base64File: (file: FileType) => ipcRenderer.invoke(IpcChannel.Gemini_Base64File, file),
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey), retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_RetrieveFile, file, apiKey),
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey), listFiles: (apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_ListFiles, apiKey),
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId) deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke(IpcChannel.Gemini_DeleteFile, apiKey, fileId)
}, },
selectionMenu: { selectionMenu: {
action: (action: string) => ipcRenderer.invoke('selection-menu:action', action) action: (action: string) => ipcRenderer.invoke(IpcChannel.SelectionMenu_Action, action)
}, },
config: { config: {
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value), set: (key: string, value: any) => ipcRenderer.invoke(IpcChannel.Config_Set, key, value),
get: (key: string) => ipcRenderer.invoke('config:get', key) get: (key: string) => ipcRenderer.invoke(IpcChannel.Config_Get, key)
}, },
miniWindow: { miniWindow: {
show: () => ipcRenderer.invoke('miniwindow:show'), show: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Show),
hide: () => ipcRenderer.invoke('miniwindow:hide'), hide: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Hide),
close: () => ipcRenderer.invoke('miniwindow:close'), close: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Close),
toggle: () => ipcRenderer.invoke('miniwindow:toggle'), toggle: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Toggle)
setPin: (isPinned: boolean) => ipcRenderer.invoke('miniwindow:set-pin', isPinned) setPin: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.MiniWindow_SetPin, isPinned)
}, },
aes: { aes: {
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv), encrypt: (text: string, secretKey: string, iv: string) =>
ipcRenderer.invoke(IpcChannel.Aes_Encrypt, text, secretKey, iv),
decrypt: (encryptedData: string, iv: string, secretKey: string) => decrypt: (encryptedData: string, iv: string, secretKey: string) =>
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey) ipcRenderer.invoke(IpcChannel.Aes_Decrypt, encryptedData, iv, secretKey)
}, },
mcp: { mcp: {
removeServer: (server: MCPServer) => ipcRenderer.invoke('mcp:remove-server', server), removeServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RemoveServer, server),
restartServer: (server: MCPServer) => ipcRenderer.invoke('mcp:restart-server', server), restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
stopServer: (server: MCPServer) => ipcRenderer.invoke('mcp:stop-server', server), stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
listTools: (server: MCPServer) => ipcRenderer.invoke('mcp:list-tools', server), listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
ipcRenderer.invoke('mcp:call-tool', { server, name, args }), ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }),
getInstallInfo: () => ipcRenderer.invoke('mcp:get-install-info') getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
}, },
shell: { shell: {
openExternal: shell.openExternal openExternal: shell.openExternal
}, },
copilot: { copilot: {
getAuthMessage: (headers?: Record<string, string>) => ipcRenderer.invoke('copilot:get-auth-message', headers), getAuthMessage: (headers?: Record<string, string>) =>
ipcRenderer.invoke(IpcChannel.Copilot_GetAuthMessage, headers),
getCopilotToken: (device_code: string, headers?: Record<string, string>) => getCopilotToken: (device_code: string, headers?: Record<string, string>) =>
ipcRenderer.invoke('copilot:get-copilot-token', device_code, headers), ipcRenderer.invoke(IpcChannel.Copilot_GetCopilotToken, device_code, headers),
saveCopilotToken: (access_token: string) => ipcRenderer.invoke('copilot:save-copilot-token', access_token), saveCopilotToken: (access_token: string) => ipcRenderer.invoke(IpcChannel.Copilot_SaveCopilotToken, access_token),
getToken: (headers?: Record<string, string>) => ipcRenderer.invoke('copilot:get-token', headers), getToken: (headers?: Record<string, string>) => ipcRenderer.invoke(IpcChannel.Copilot_GetToken, headers),
logout: () => ipcRenderer.invoke('copilot:logout'), logout: () => ipcRenderer.invoke(IpcChannel.Copilot_Logout),
getUser: (token: string) => ipcRenderer.invoke('copilot:get-user', token) getUser: (token: string) => ipcRenderer.invoke(IpcChannel.Copilot_GetUser, token)
}, },
// Binary related APIs // Binary related APIs
isBinaryExist: (name: string) => ipcRenderer.invoke('app:is-binary-exist', name), isBinaryExist: (name: string) => ipcRenderer.invoke(IpcChannel.App_IsBinaryExist, name),
getBinaryPath: (name: string) => ipcRenderer.invoke('app:get-binary-path', name), getBinaryPath: (name: string) => ipcRenderer.invoke(IpcChannel.App_GetBinaryPath, name),
installUVBinary: () => ipcRenderer.invoke('app:install-uv-binary'), installUVBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallUvBinary),
installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary'), installBunBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallBunBinary)
protocol: { protocol: {
onReceiveData: (callback: (data: { url: string; params: any }) => void) => { onReceiveData: (callback: (data: { url: string; params: any }) => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: { url: string; params: any }) => { const listener = (_event: Electron.IpcRendererEvent, data: { url: string; params: any }) => {
@ -159,10 +163,10 @@ const api = {
} }
}, },
nutstore: { nutstore: {
getSSOUrl: () => ipcRenderer.invoke('nutstore:get-sso-url'), getSSOUrl: () => ipcRenderer.invoke(IpcChannel.Nutstore_GetSsoUrl),
decryptToken: (token: string) => ipcRenderer.invoke('nutstore:decrypt-token', token), decryptToken: (token: string) => ipcRenderer.invoke(IpcChannel.Nutstore_DecryptToken, token),
getDirectoryContents: (token: string, path: string) => getDirectoryContents: (token: string, path: string) =>
ipcRenderer.invoke('nutstore:get-directory-contents', token, path) ipcRenderer.invoke(IpcChannel.Nutstore_GetDirectoryContents, token, path)
} }
} }
@ -174,9 +178,9 @@ if (process.contextIsolated) {
contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api) contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('obsidian', { contextBridge.exposeInMainWorld('obsidian', {
getVaults: () => ipcRenderer.invoke('obsidian:get-vaults'), getVaults: () => ipcRenderer.invoke(IpcChannel.Obsidian_GetVaults),
getFolders: (vaultName: string) => ipcRenderer.invoke('obsidian:get-files', vaultName), getFolders: (vaultName: string) => ipcRenderer.invoke(IpcChannel.Obsidian_GetFiles, vaultName),
getFiles: (vaultName: string) => ipcRenderer.invoke('obsidian:get-files', vaultName) getFiles: (vaultName: string) => ipcRenderer.invoke(IpcChannel.Obsidian_GetFiles, vaultName)
}) })
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TopView } from '../TopView' import { TopView } from '../TopView'
import { IpcChannel } from '@shared/IpcChannel'
interface Props { interface Props {
resolve: (data: any) => void resolve: (data: any) => void
@ -21,7 +22,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const { t } = useTranslation() const { t } = useTranslation()
useEffect(() => { useEffect(() => {
const removeListener = window.electron.ipcRenderer.on('backup-progress', (_, data: ProgressData) => { const removeListener = window.electron.ipcRenderer.on(IpcChannel.BackupProgress, (_, data: ProgressData) => {
setProgressData(data) setProgressData(data)
}) })

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TopView } from '../TopView' import { TopView } from '../TopView'
import { IpcChannel } from '@shared/IpcChannel'
interface Props { interface Props {
resolve: (data: any) => void resolve: (data: any) => void
@ -21,7 +22,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const { t } = useTranslation() const { t } = useTranslation()
useEffect(() => { useEffect(() => {
const removeListener = window.electron.ipcRenderer.on('restore-progress', (_, data: ProgressData) => { const removeListener = window.electron.ipcRenderer.on(IpcChannel.RestoreProgress, (_, data: ProgressData) => {
setProgressData(data) setProgressData(data)
}) })

View File

@ -2,6 +2,7 @@ import { isMac } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import React, { createContext, PropsWithChildren, use, useEffect, useState } from 'react' import React, { createContext, PropsWithChildren, use, useEffect, useState } from 'react'
import { IpcChannel } from '@shared/IpcChannel'
interface ThemeContextType { interface ThemeContextType {
theme: ThemeMode theme: ThemeMode
@ -49,7 +50,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
document.body.setAttribute('os', isMac ? 'mac' : 'windows') document.body.setAttribute('os', isMac ? 'mac' : 'windows')
// listen theme change from main process from other windows // listen theme change from main process from other windows
const themeChangeListenerRemover = window.electron.ipcRenderer.on('theme:change', (_, newTheme) => { const themeChangeListenerRemover = window.electron.ipcRenderer.on(IpcChannel.ThemeChange, (_, newTheme) => {
setTheme(newTheme) setTheme(newTheme)
}) })
return () => { return () => {

View File

@ -15,6 +15,8 @@ import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings' import { useSettings } from './useSettings'
import useUpdateHandler from './useUpdateHandler' import useUpdateHandler from './useUpdateHandler'
import { defaultLanguage } from '@shared/config/constant'
export function useAppInit() { export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { proxyUrl, language, windowStyle, autoCheckUpdate, proxyMode, customCss } = useSettings() const { proxyUrl, language, windowStyle, autoCheckUpdate, proxyMode, customCss } = useSettings()
@ -53,7 +55,7 @@ export function useAppInit() {
}, [proxyUrl, proxyMode]) }, [proxyUrl, proxyMode])
useEffect(() => { useEffect(() => {
i18n.changeLanguage(language || navigator.language || 'en-US') i18n.changeLanguage(language || navigator.language || defaultLanguage)
}, [language]) }, [language])
useEffect(() => { useEffect(() => {

View File

@ -1,12 +1,13 @@
import { isWindows } from '@renderer/config/constant' import { isWindows } from '@renderer/config/constant'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { IpcChannel } from '@shared/IpcChannel'
export function useFullScreenNotice() { export function useFullScreenNotice() {
const { t } = useTranslation() const { t } = useTranslation()
useEffect(() => { useEffect(() => {
const cleanup = window.electron.ipcRenderer.on('fullscreen-status-changed', (_, isFullscreen) => { const cleanup = window.electron.ipcRenderer.on(IpcChannel.FullscreenStatusChanged, (_, isFullscreen) => {
if (isWindows && isFullscreen) { if (isWindows && isFullscreen) {
window.message.info({ window.message.info({
content: t('common.fullscreen'), content: t('common.fullscreen'),

View File

@ -27,6 +27,7 @@ import { v4 as uuidv4 } from 'uuid'
import { useAgents } from './useAgents' import { useAgents } from './useAgents'
import { useAssistants } from './useAssistant' import { useAssistants } from './useAssistant'
import { IpcChannel } from '@shared/IpcChannel'
export const useKnowledge = (baseId: string) => { export const useKnowledge = (baseId: string) => {
const dispatch = useDispatch() const dispatch = useDispatch()
@ -207,7 +208,7 @@ export const useKnowledge = (baseId: string) => {
} }
const cleanup = window.electron.ipcRenderer.on( const cleanup = window.electron.ipcRenderer.on(
'directory-processing-percent', IpcChannel.DirectoryProcessingPercent,
(_, { itemId: id, percent }: { itemId: string; percent: number }) => { (_, { itemId: id, percent }: { itemId: string; percent: number }) => {
if (itemId === id) { if (itemId === id) {
setPercent(percent) setPercent(percent)

View File

@ -1,11 +1,12 @@
import store, { useAppDispatch, useAppSelector } from '@renderer/store' import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { addMCPServer, deleteMCPServer, setMCPServers, updateMCPServer } from '@renderer/store/mcp' import { addMCPServer, deleteMCPServer, setMCPServers, updateMCPServer } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { IpcChannel } from '@shared/IpcChannel'
const ipcRenderer = window.electron.ipcRenderer const ipcRenderer = window.electron.ipcRenderer
// Listen for server changes from main process // Listen for server changes from main process
ipcRenderer.on('mcp:servers-changed', (_event, servers) => { ipcRenderer.on(IpcChannel.Mcp_ServersChanged, (_event, servers) => {
store.dispatch(setMCPServers(servers)) store.dispatch(setMCPServers(servers))
}) })

View File

@ -3,6 +3,7 @@ import { setUpdateState } from '@renderer/store/runtime'
import type { ProgressInfo, UpdateInfo } from 'builder-util-runtime' import type { ProgressInfo, UpdateInfo } from 'builder-util-runtime'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { IpcChannel } from '@shared/IpcChannel'
export default function useUpdateHandler() { export default function useUpdateHandler() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -14,13 +15,13 @@ export default function useUpdateHandler() {
const ipcRenderer = window.electron.ipcRenderer const ipcRenderer = window.electron.ipcRenderer
const removers = [ const removers = [
ipcRenderer.on('update-not-available', () => { ipcRenderer.on(IpcChannel.UpdateNotAvailable, () => {
dispatch(setUpdateState({ checking: false })) dispatch(setUpdateState({ checking: false }))
if (window.location.hash.includes('settings/about')) { if (window.location.hash.includes('settings/about')) {
window.message.success(t('settings.about.updateNotAvailable')) window.message.success(t('settings.about.updateNotAvailable'))
} }
}), }),
ipcRenderer.on('update-available', (_, releaseInfo: UpdateInfo) => { ipcRenderer.on(IpcChannel.UpdateAvailable, (_, releaseInfo: UpdateInfo) => {
dispatch( dispatch(
setUpdateState({ setUpdateState({
checking: false, checking: false,
@ -30,7 +31,7 @@ export default function useUpdateHandler() {
}) })
) )
}), }),
ipcRenderer.on('download-update', () => { ipcRenderer.on(IpcChannel.DownloadUpdate, () => {
dispatch( dispatch(
setUpdateState({ setUpdateState({
checking: false, checking: false,
@ -38,7 +39,7 @@ export default function useUpdateHandler() {
}) })
) )
}), }),
ipcRenderer.on('download-progress', (_, progress: ProgressInfo) => { ipcRenderer.on(IpcChannel.DownloadProgress, (_, progress: ProgressInfo) => {
dispatch( dispatch(
setUpdateState({ setUpdateState({
downloading: progress.percent < 100, downloading: progress.percent < 100,
@ -46,7 +47,7 @@ export default function useUpdateHandler() {
}) })
) )
}), }),
ipcRenderer.on('update-downloaded', (_, releaseInfo: UpdateInfo) => { ipcRenderer.on(IpcChannel.UpdateDownloaded, (_, releaseInfo: UpdateInfo) => {
dispatch( dispatch(
setUpdateState({ setUpdateState({
downloading: false, downloading: false,
@ -55,7 +56,7 @@ export default function useUpdateHandler() {
}) })
) )
}), }),
ipcRenderer.on('update-error', (_, error) => { ipcRenderer.on(IpcChannel.UpdateError, (_, error) => {
dispatch( dispatch(
setUpdateState({ setUpdateState({
checking: false, checking: false,

View File

@ -13,6 +13,8 @@ import esES from './translate/es-es.json'
import frFR from './translate/fr-fr.json' import frFR from './translate/fr-fr.json'
import ptPT from './translate/pt-pt.json' import ptPT from './translate/pt-pt.json'
import { defaultLanguage } from '@shared/config/constant'
const resources = { const resources = {
'el-GR': elGR, 'el-GR': elGR,
'en-US': enUS, 'en-US': enUS,
@ -26,7 +28,7 @@ const resources = {
} }
export const getLanguage = () => { export const getLanguage = () => {
return localStorage.getItem('language') || navigator.language || 'en-US' return localStorage.getItem('language') || navigator.language || defaultLanguage
} }
export const getLanguageCode = () => { export const getLanguageCode = () => {
@ -36,7 +38,7 @@ export const getLanguageCode = () => {
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources, resources,
lng: getLanguage(), lng: getLanguage(),
fallbackLng: 'en-US', fallbackLng: defaultLanguage,
interpolation: { interpolation: {
escapeValue: false escapeValue: false
} }

View File

@ -8,6 +8,7 @@ import { FC, useCallback, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import FileItem from './FileItem' import FileItem from './FileItem'
import { MB } from '@shared/config/constant'
interface GeminiFilesProps { interface GeminiFilesProps {
id: string id: string
@ -60,7 +61,7 @@ const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
fileInfo={{ fileInfo={{
name: file.displayName, name: file.displayName,
ext: `.${file.name.split('.').pop()}`, ext: `.${file.name.split('.').pop()}`,
extra: `${dayjs(file.createTime).format('MM-DD HH:mm')} · ${(parseInt(file.sizeBytes) / 1024 / 1024).toFixed(2)} MB`, extra: `${dayjs(file.createTime).format('MM-DD HH:mm')} · ${(parseInt(file.sizeBytes) / MB).toFixed(2)} MB`,
actions: ( actions: (
<DeleteOutlined <DeleteOutlined
style={{ cursor: 'pointer', color: 'var(--color-error)' }} style={{ cursor: 'pointer', color: 'var(--color-error)' }}

View File

@ -11,6 +11,7 @@ import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
import { defaultLanguage } from '@shared/config/constant'
const GeneralSettings: FC = () => { const GeneralSettings: FC = () => {
const { const {
@ -112,7 +113,7 @@ const GeneralSettings: FC = () => {
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('common.language')}</SettingRowTitle> <SettingRowTitle>{t('common.language')}</SettingRowTitle>
<Select defaultValue={language || 'en-US'} style={{ width: 180 }} onChange={onSelectLanguage}> <Select defaultValue={language || defaultLanguage} style={{ width: 180 }} onChange={onSelectLanguage}>
{languagesOptions.map((lang) => ( {languagesOptions.map((lang) => (
<Select.Option key={lang.value} value={lang.value}> <Select.Option key={lang.value} value={lang.value}>
<Space.Compact direction="horizontal" block> <Space.Compact direction="horizontal" block>

View File

@ -44,6 +44,7 @@ import OpenAI from 'openai'
import { ChunkCallbackData, CompletionsParams } from '.' import { ChunkCallbackData, CompletionsParams } from '.'
import BaseProvider from './BaseProvider' import BaseProvider from './BaseProvider'
import { MB } from '@shared/config/constant'
export default class GeminiProvider extends BaseProvider { export default class GeminiProvider extends BaseProvider {
private sdk: GoogleGenerativeAI private sdk: GoogleGenerativeAI
@ -70,7 +71,7 @@ export default class GeminiProvider extends BaseProvider {
* @returns The part * @returns The part
*/ */
private async handlePdfFile(file: FileType): Promise<Part> { private async handlePdfFile(file: FileType): Promise<Part> {
const smallFileSize = 20 * 1024 * 1024 const smallFileSize = 20 * MB
const isSmallFile = file.size < smallFileSize const isSmallFile = file.size < smallFileSize
if (isSmallFile) { if (isSmallFile) {

View File

@ -3,6 +3,7 @@ import { isLocalAi } from '@renderer/config/env'
import { SYSTEM_MODELS } from '@renderer/config/models' import { SYSTEM_MODELS } from '@renderer/config/models'
import { Model, Provider } from '@renderer/types' import { Model, Provider } from '@renderer/types'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
import { IpcChannel } from '@shared/IpcChannel'
type LlmSettings = { type LlmSettings = {
ollama: { ollama: {
@ -572,7 +573,7 @@ const settingsSlice = createSlice({
}, },
setDefaultModel: (state, action: PayloadAction<{ model: Model }>) => { setDefaultModel: (state, action: PayloadAction<{ model: Model }>) => {
state.defaultModel = action.payload.model state.defaultModel = action.payload.model
window.electron.ipcRenderer.send('miniwindow-reload') window.electron.ipcRenderer.send(IpcChannel.MiniWindowReload)
}, },
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => { setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
state.topicNamingModel = action.payload.model state.topicNamingModel = action.payload.model

View File

@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts' import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { CodeStyleVarious, LanguageVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types' import { CodeStyleVarious, LanguageVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
import { IpcChannel } from '@shared/IpcChannel'
import { WebDAVSyncState } from './backup' import { WebDAVSyncState } from './backup'
@ -208,7 +209,7 @@ const settingsSlice = createSlice({
}, },
setLanguage: (state, action: PayloadAction<LanguageVarious>) => { setLanguage: (state, action: PayloadAction<LanguageVarious>) => {
state.language = action.payload state.language = action.payload
window.electron.ipcRenderer.send('miniwindow-reload') window.electron.ipcRenderer.send(IpcChannel.MiniWindowReload)
}, },
setTargetLanguage: (state, action: PayloadAction<TranslateLanguageVarious>) => { setTargetLanguage: (state, action: PayloadAction<TranslateLanguageVarious>) => {
state.targetLanguage = action.payload state.targetLanguage = action.payload

View File

@ -7,6 +7,7 @@ import * as htmlToImage from 'html-to-image'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { classNames } from './style' import { classNames } from './style'
import { KB, MB } from '@shared/config/constant'
export const runAsyncFunction = async (fn: () => void) => { export const runAsyncFunction = async (fn: () => void) => {
await fn() await fn()
@ -421,15 +422,15 @@ export function hasPath(url: string): boolean {
} }
export function formatFileSize(size: number) { export function formatFileSize(size: number) {
if (size > 1024 * 1024) { if (size > MB) {
return (size / 1024 / 1024).toFixed(1) + ' MB' return (size / MB).toFixed(1) + ' MB'
} }
if (size > 1024) { if (size > KB) {
return (size / 1024).toFixed(0) + ' KB' return (size / KB).toFixed(0) + ' KB'
} }
return (size / 1024).toFixed(2) + ' KB' return (size / KB).toFixed(2) + ' KB'
} }
export function sortByEnglishFirst(a: string, b: string) { export function sortByEnglishFirst(a: string, b: string) {

View File

@ -18,6 +18,9 @@ import ClipboardPreview from './components/ClipboardPreview'
import FeatureMenus, { FeatureMenusRef } from './components/FeatureMenus' import FeatureMenus, { FeatureMenusRef } from './components/FeatureMenus'
import Footer from './components/Footer' import Footer from './components/Footer'
import InputBar from './components/InputBar' import InputBar from './components/InputBar'
import { IpcChannel } from '@shared/IpcChannel'
import { defaultLanguage } from '@shared/config/constant'
const HomeWindow: FC = () => { const HomeWindow: FC = () => {
const [route, setRoute] = useState<'home' | 'chat' | 'translate' | 'summary' | 'explanation'>('home') const [route, setRoute] = useState<'home' | 'chat' | 'translate' | 'summary' | 'explanation'>('home')
@ -68,7 +71,7 @@ const HomeWindow: FC = () => {
}, [readClipboard]) }, [readClipboard])
useEffect(() => { useEffect(() => {
i18n.changeLanguage(language || navigator.language || 'en-US') i18n.changeLanguage(language || navigator.language || defaultLanguage)
}, [language]) }, [language])
const onCloseWindow = () => window.api.miniWindow.hide() const onCloseWindow = () => window.api.miniWindow.hide()
@ -181,16 +184,16 @@ const HomeWindow: FC = () => {
}) })
useEffect(() => { useEffect(() => {
window.electron.ipcRenderer.on('show-mini-window', onWindowShow) window.electron.ipcRenderer.on(IpcChannel.ShowMiniWindow, onWindowShow)
window.electron.ipcRenderer.on('selection-action', (_, { action, selectedText }) => { window.electron.ipcRenderer.on(IpcChannel.SelectionAction, (_, { action, selectedText }) => {
selectedText && setSelectedText(selectedText) selectedText && setSelectedText(selectedText)
action && setRoute(action) action && setRoute(action)
action === 'chat' && onSendMessage() action === 'chat' && onSendMessage()
}) })
return () => { return () => {
window.electron.ipcRenderer.removeAllListeners('show-mini-window') window.electron.ipcRenderer.removeAllListeners(IpcChannel.ShowMiniWindow)
window.electron.ipcRenderer.removeAllListeners('selection-action') window.electron.ipcRenderer.removeAllListeners(IpcChannel.SelectionAction)
} }
}, [onWindowShow, onSendMessage, setRoute]) }, [onWindowShow, onSendMessage, setRoute])