mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-30 07:39:06 +08:00
368 lines
13 KiB
TypeScript
368 lines
13 KiB
TypeScript
// don't reorder this file, it's used to initialize the app data dir and
|
|
// other which should be run before the main process is ready
|
|
// eslint-disable-next-line
|
|
import './bootstrap'
|
|
|
|
import '@main/config'
|
|
|
|
import { loggerService } from '@logger'
|
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
|
import { dbService } from '@data/db/DbService'
|
|
import { preferenceService } from '@data/PreferenceService'
|
|
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
|
import { app, dialog, crashReporter } from 'electron'
|
|
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
|
import { isDev, isLinux, isWin } from './constant'
|
|
|
|
import process from 'node:process'
|
|
|
|
import { registerIpc } from './ipc'
|
|
import { agentService } from './services/agents'
|
|
import { apiServerService } from './services/ApiServerService'
|
|
import { appMenuService } from './services/AppMenuService'
|
|
import { lanTransferClientService } from './services/lanTransfer'
|
|
import mcpService from './services/MCPService'
|
|
import { localTransferService } from './services/LocalTransferService'
|
|
import { nodeTraceService } from './services/NodeTraceService'
|
|
import powerMonitorService from './services/PowerMonitorService'
|
|
import {
|
|
CHERRY_STUDIO_PROTOCOL,
|
|
handleProtocolUrl,
|
|
registerProtocolClient,
|
|
setupAppImageDeepLink
|
|
} from './services/ProtocolClient'
|
|
import selectionService, { initSelectionService } from './services/SelectionService'
|
|
import { registerShortcuts } from './services/ShortcutService'
|
|
import { TrayService } from './services/TrayService'
|
|
import { versionService } from './services/VersionService'
|
|
import { windowService } from './services/WindowService'
|
|
import {
|
|
getAllMigrators,
|
|
migrationEngine,
|
|
migrationWindowManager,
|
|
registerMigrationIpcHandlers,
|
|
unregisterMigrationIpcHandlers
|
|
} from '@data/migration/v2'
|
|
import { dataApiService } from '@data/DataApiService'
|
|
import { cacheService } from '@data/CacheService'
|
|
import { initWebviewHotkeys } from './services/WebviewService'
|
|
import { runAsyncFunction } from './utils'
|
|
import { ovmsManager } from './services/OvmsManager'
|
|
|
|
const logger = loggerService.withContext('MainEntry')
|
|
|
|
// enable local crash reports
|
|
crashReporter.start({
|
|
companyName: 'CherryHQ',
|
|
productName: 'CherryStudio',
|
|
submitURL: '',
|
|
uploadToServer: false
|
|
})
|
|
|
|
/**
|
|
* Disable hardware acceleration if setting is enabled
|
|
*/
|
|
//FIXME should not use preferenceService before initialization
|
|
//TODO 我们需要调整配置管理的加载位置,以保证其在 preferenceService 初始化之前被调用
|
|
// const disableHardwareAcceleration = preferenceService.get('app.disable_hardware_acceleration')
|
|
// if (disableHardwareAcceleration) {
|
|
// app.disableHardwareAcceleration()
|
|
// }
|
|
|
|
/**
|
|
* Disable chromium's window animations
|
|
* main purpose for this is to avoid the transparent window flashing when it is shown
|
|
* (especially on Windows for SelectionAssistant Toolbar)
|
|
* Know Issue: https://github.com/electron/electron/issues/12130#issuecomment-627198990
|
|
*/
|
|
if (isWin) {
|
|
app.commandLine.appendSwitch('wm-window-animations-disabled')
|
|
}
|
|
|
|
/**
|
|
* Enable GlobalShortcutsPortal for Linux Wayland Protocol
|
|
* see: https://www.electronjs.org/docs/latest/api/global-shortcut
|
|
*/
|
|
if (isLinux && process.env.XDG_SESSION_TYPE === 'wayland') {
|
|
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal')
|
|
}
|
|
|
|
// DocumentPolicyIncludeJSCallStacksInCrashReports: Enable features for unresponsive renderer js call stacks
|
|
// EarlyEstablishGpuChannel,EstablishGpuChannelAsync: Enable features for early establish gpu channel
|
|
// speed up the startup time
|
|
// https://github.com/microsoft/vscode/pull/241640/files
|
|
app.commandLine.appendSwitch(
|
|
'enable-features',
|
|
'DocumentPolicyIncludeJSCallStacksInCrashReports,EarlyEstablishGpuChannel,EstablishGpuChannelAsync'
|
|
)
|
|
app.on('web-contents-created', (_, webContents) => {
|
|
webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
|
callback({
|
|
responseHeaders: {
|
|
...details.responseHeaders,
|
|
'Document-Policy': ['include-js-call-stacks-in-crash-reports']
|
|
}
|
|
})
|
|
})
|
|
|
|
webContents.on('unresponsive', async () => {
|
|
// Interrupt execution and collect call stack from unresponsive renderer
|
|
logger.error('Renderer unresponsive start')
|
|
const callStack = await webContents.mainFrame.collectJavaScriptCallStack()
|
|
logger.error(`Renderer unresponsive js call stack\n ${callStack}`)
|
|
})
|
|
})
|
|
|
|
// in production mode, handle uncaught exception and unhandled rejection globally
|
|
if (!isDev) {
|
|
// handle uncaught exception
|
|
process.on('uncaughtException', (error) => {
|
|
logger.error('Uncaught Exception:', error)
|
|
})
|
|
|
|
// handle unhandled rejection
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
logger.error(`Unhandled Rejection at: ${promise} reason: ${reason}`)
|
|
})
|
|
}
|
|
|
|
// Check for single instance lock
|
|
if (!app.requestSingleInstanceLock()) {
|
|
app.quit()
|
|
process.exit(0)
|
|
} else {
|
|
// This method will be called when Electron has finished
|
|
// initialization and is ready to create browser windows.
|
|
// Some APIs can only be used after this event occurs.
|
|
app.whenReady().then(async () => {
|
|
//TODO v2 Data Refactor: App Lifecycle Management
|
|
// This is the temporary solution for the data migration v2.
|
|
// We will refactor the app lifecycle management after the data migration v2 is stable.
|
|
|
|
// First of all, init & migrate the database
|
|
try {
|
|
await dbService.init()
|
|
await dbService.migrateDb()
|
|
await dbService.migrateSeed('preference')
|
|
} catch (error) {
|
|
logger.error('Failed to initialize database', error as Error)
|
|
//TODO for v2 testing only:
|
|
await dialog.showErrorBox(
|
|
'Database Initialization Failed',
|
|
'Before the official release of the alpha version, the database structure may change at any time. To maintain simplicity, the database migration files will be periodically reinitialized, which may cause the application to fail. If this occurs, please delete the cherrystudio.sqlite file located in the user data directory.'
|
|
)
|
|
app.quit()
|
|
return
|
|
}
|
|
|
|
// Data Migration v2
|
|
// Check if data migration is needed BEFORE creating any windows
|
|
try {
|
|
logger.info('Checking if data migration v2 is needed')
|
|
|
|
// Register migration IPC handlers
|
|
registerMigrationIpcHandlers()
|
|
|
|
// Register migrators
|
|
migrationEngine.registerMigrators(getAllMigrators())
|
|
|
|
const needsMigration = await migrationEngine.needsMigration()
|
|
logger.info('Migration status check result', { needsMigration })
|
|
|
|
if (needsMigration) {
|
|
logger.info('Data Migration v2 needed, starting migration process')
|
|
|
|
try {
|
|
// Create and show migration window
|
|
migrationWindowManager.create()
|
|
await migrationWindowManager.waitForReady()
|
|
logger.info('Migration window created successfully')
|
|
// Migration window will handle the flow, no need to continue startup
|
|
return
|
|
} catch (migrationError) {
|
|
logger.error('Failed to start migration process', migrationError as Error)
|
|
|
|
// Cleanup IPC handlers on failure
|
|
unregisterMigrationIpcHandlers()
|
|
|
|
// Migration is required for this version - show error and exit
|
|
await dialog.showErrorBox(
|
|
'Migration Required - Application Cannot Start',
|
|
`This version of Cherry Studio requires data migration to function properly.\n\nMigration window failed to start: ${(migrationError as Error).message}\n\nThe application will now exit. Please try starting again or contact support if the problem persists.`
|
|
)
|
|
|
|
logger.error('Exiting application due to failed migration startup')
|
|
app.quit()
|
|
return
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Migration status check failed', error as Error)
|
|
|
|
// If we can't check migration status, this could indicate a serious database issue
|
|
// Since migration may be required, it's safer to exit and let user investigate
|
|
await dialog.showErrorBox(
|
|
'Migration Status Check Failed - Application Cannot Start',
|
|
`Could not determine if data migration is completed.\n\nThis may indicate a database connectivity issue: ${(error as Error).message}\n\nThe application will now exit. Please check your installation and try again.`
|
|
)
|
|
|
|
logger.error('Exiting application due to migration status check failure')
|
|
app.quit()
|
|
return
|
|
}
|
|
|
|
// DATA REFACTOR USE
|
|
// TODO: remove when data refactor is stable
|
|
//************FOR TESTING ONLY START****************/
|
|
|
|
await preferenceService.initialize()
|
|
|
|
// Initialize DataApiService
|
|
await dataApiService.initialize()
|
|
|
|
// Initialize CacheService
|
|
await cacheService.initialize()
|
|
|
|
/************FOR TESTING ONLY END****************/
|
|
|
|
// Record current version for tracking
|
|
// A preparation for v2 data refactoring
|
|
versionService.recordCurrentVersion()
|
|
|
|
initWebviewHotkeys()
|
|
// Set app user model id for windows
|
|
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
|
|
|
// Mac: Hide dock icon before window creation when launch to tray is set
|
|
const isLaunchToTray = preferenceService.get('app.tray.on_launch')
|
|
if (isLaunchToTray) {
|
|
app.dock?.hide()
|
|
}
|
|
|
|
// Create main window - migration has either completed or was not needed
|
|
const mainWindow = windowService.createMainWindow()
|
|
|
|
new TrayService()
|
|
|
|
// Setup macOS application menu
|
|
appMenuService?.setupApplicationMenu()
|
|
nodeTraceService.init()
|
|
powerMonitorService.init()
|
|
|
|
app.on('activate', function () {
|
|
const mainWindow = windowService.getMainWindow()
|
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
windowService.createMainWindow()
|
|
} else {
|
|
windowService.showMainWindow()
|
|
}
|
|
})
|
|
|
|
registerShortcuts(mainWindow)
|
|
registerIpc(mainWindow, app)
|
|
localTransferService.startDiscovery({ resetList: true })
|
|
|
|
replaceDevtoolsFont(mainWindow)
|
|
|
|
// Setup deep link for AppImage on Linux
|
|
await setupAppImageDeepLink()
|
|
|
|
if (isDev) {
|
|
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
|
|
.then((name) => logger.info(`Added Extension: ${name}`))
|
|
.catch((err) => logger.error('An error occurred: ', err))
|
|
}
|
|
|
|
//start selection assistant service
|
|
initSelectionService()
|
|
|
|
runAsyncFunction(async () => {
|
|
// Start API server if enabled or if agents exist
|
|
try {
|
|
const config = await apiServerService.getCurrentConfig()
|
|
logger.info('API server config:', config)
|
|
|
|
// Check if there are any agents
|
|
let shouldStart = config.enabled
|
|
if (!shouldStart) {
|
|
try {
|
|
const { total } = await agentService.listAgents({ limit: 1 })
|
|
if (total > 0) {
|
|
shouldStart = true
|
|
logger.info(`Detected ${total} agent(s), auto-starting API server`)
|
|
}
|
|
} catch (error: any) {
|
|
logger.warn('Failed to check agent count:', error)
|
|
}
|
|
}
|
|
|
|
if (shouldStart) {
|
|
await apiServerService.start()
|
|
}
|
|
} catch (error: any) {
|
|
logger.error('Failed to check/start API server:', error)
|
|
}
|
|
})
|
|
})
|
|
|
|
registerProtocolClient(app)
|
|
|
|
// macOS specific: handle protocol when app is already running
|
|
|
|
app.on('open-url', (event, url) => {
|
|
event.preventDefault()
|
|
handleProtocolUrl(url)
|
|
})
|
|
|
|
const handleOpenUrl = (args: string[]) => {
|
|
const url = args.find((arg) => arg.startsWith(CHERRY_STUDIO_PROTOCOL + '://'))
|
|
if (url) handleProtocolUrl(url)
|
|
}
|
|
|
|
// for windows to start with url
|
|
handleOpenUrl(process.argv)
|
|
|
|
// Listen for second instance
|
|
app.on('second-instance', (_event, argv) => {
|
|
windowService.showMainWindow()
|
|
|
|
// Protocol handler for Windows/Linux
|
|
// The commandLine is an array of strings where the last item might be the URL
|
|
handleOpenUrl(argv)
|
|
})
|
|
|
|
app.on('browser-window-created', (_, window) => {
|
|
optimizer.watchWindowShortcuts(window)
|
|
})
|
|
|
|
app.on('before-quit', () => {
|
|
app.isQuitting = true
|
|
|
|
// quit selection service
|
|
if (selectionService) {
|
|
selectionService.quit()
|
|
}
|
|
|
|
lanTransferClientService.dispose()
|
|
localTransferService.dispose()
|
|
})
|
|
|
|
app.on('will-quit', async () => {
|
|
// 简单的资源清理,不阻塞退出流程
|
|
await ovmsManager.stopOvms()
|
|
|
|
try {
|
|
await mcpService.cleanup()
|
|
await apiServerService.stop()
|
|
} catch (error) {
|
|
logger.warn('Error cleaning up MCP service:', error as Error)
|
|
}
|
|
|
|
// finish the logger
|
|
logger.finish()
|
|
})
|
|
|
|
// In this file you can include the rest of your app"s specific main process
|
|
// code. You can also put them in separate files and require them here.
|
|
}
|