mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
178 lines
5.2 KiB
TypeScript
178 lines
5.2 KiB
TypeScript
import { app, BrowserWindow, protocol, dialog } from 'electron'
|
|
import { join } from 'path'
|
|
import { optimizer, platform } from '@electron-toolkit/utils'
|
|
import { checkUpdate } from './update/manager'
|
|
import mainIpcMain, { cleanup } from './ipc'
|
|
import { startInternalServer, stopInternalServer, registerInternalApiIpc } from './internal-api/server'
|
|
import { getDataSourceManager, getPullEngine } from './ipc/api'
|
|
import { getPathProvider } from './paths/provider'
|
|
import { initAnalytics } from './analytics'
|
|
import { logger } from './logger'
|
|
import { prepareDesktopRuntime } from './app/bootstrap'
|
|
import { createMainWindow, markAppQuitting } from './window/main-window'
|
|
import { destroyWindowsTray } from './window/windows-tray'
|
|
import { initLockManager, cleanupLockManager } from './security/lock-manager'
|
|
|
|
class MainProcess {
|
|
mainWindow: BrowserWindow | null
|
|
isTestMode: boolean
|
|
|
|
constructor() {
|
|
this.mainWindow = null
|
|
this.isTestMode = process.env.TEST_MODE === 'true'
|
|
|
|
// Isolate E2E instances to avoid sharing state, locks, and databases.
|
|
const e2eUserDataDir = process.env.CHATLAB_E2E_USER_DATA_DIR
|
|
if (this.isTestMode && e2eUserDataDir) {
|
|
app.setPath('userData', e2eUserDataDir)
|
|
} else if (!this.isTestMode && e2eUserDataDir) {
|
|
console.warn('[Main] Ignored CHATLAB_E2E_USER_DATA_DIR because TEST_MODE is not enabled')
|
|
}
|
|
|
|
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
|
this.checkApp().then(async (lockObtained) => {
|
|
if (lockObtained) {
|
|
await this.init()
|
|
}
|
|
})
|
|
}
|
|
|
|
async checkApp(): Promise<boolean> {
|
|
if (this.isTestMode) {
|
|
return true
|
|
}
|
|
|
|
if (!app.requestSingleInstanceLock()) {
|
|
app.quit()
|
|
return false
|
|
}
|
|
|
|
app.on('second-instance', () => {
|
|
if (this.mainWindow) {
|
|
this.mainWindow.show()
|
|
if (this.mainWindow.isMinimized()) this.mainWindow.restore()
|
|
this.mainWindow.focus()
|
|
}
|
|
})
|
|
return true
|
|
}
|
|
|
|
async init(): Promise<void> {
|
|
initAnalytics()
|
|
logger.info('Desktop app starting')
|
|
|
|
if (!(await prepareDesktopRuntime(this.isTestMode))) {
|
|
return
|
|
}
|
|
|
|
protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }])
|
|
this.registerAppEvents()
|
|
}
|
|
|
|
async createWindow(): Promise<void> {
|
|
this.mainWindow = await createMainWindow({
|
|
preloadPath: join(__dirname, '../preload/index.js'),
|
|
rendererHtmlPath: join(__dirname, '../../out/renderer/index.html'),
|
|
})
|
|
}
|
|
|
|
registerAppEvents(): void {
|
|
app.whenReady().then(async () => {
|
|
console.log('[Main] App is ready')
|
|
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
|
|
|
try {
|
|
await startInternalServer(getPathProvider(), { getDataSourceManager, getPullEngine })
|
|
registerInternalApiIpc()
|
|
console.log('[Main] Internal API Server ready')
|
|
} catch (error) {
|
|
console.error('[Main] Internal API Server failed to start:', error)
|
|
dialog.showErrorBox(
|
|
'ChatLab Internal Server Error',
|
|
`Internal API Server failed to start. The application cannot continue.\n\n${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`
|
|
)
|
|
app.quit()
|
|
return
|
|
}
|
|
|
|
console.log('[Main] Creating window...')
|
|
await this.createWindow()
|
|
console.log('[Main] Window created')
|
|
|
|
if (this.mainWindow) {
|
|
checkUpdate(this.mainWindow)
|
|
console.log('[Main] Registering IPC handlers...')
|
|
mainIpcMain(this.mainWindow)
|
|
console.log('[Main] IPC handlers registered')
|
|
|
|
// 初始化应用锁(在 IPC 注册之后)
|
|
console.log('[Main] Initializing app lock...')
|
|
initLockManager(this.mainWindow)
|
|
console.log('[Main] App lock initialized')
|
|
}
|
|
|
|
app.on('browser-window-created', (_, window) => {
|
|
optimizer.watchWindowShortcuts(window)
|
|
})
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
void this.createWindow()
|
|
return
|
|
}
|
|
|
|
if (platform.isMacOS) {
|
|
this.mainWindow?.show()
|
|
}
|
|
})
|
|
|
|
app.on('render-process-gone', (_event, webContents, details) => {
|
|
if (details.reason === 'crashed') {
|
|
webContents.reload()
|
|
}
|
|
})
|
|
|
|
app.on('open-url', (_, url) => {
|
|
console.log('Received custom protocol URL:', url)
|
|
})
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (!platform.isMacOS) {
|
|
app.quit()
|
|
}
|
|
})
|
|
|
|
app.on('before-quit', () => {
|
|
markAppQuitting()
|
|
})
|
|
|
|
app.on('will-quit', () => {
|
|
destroyWindowsTray()
|
|
stopInternalServer().catch(() => {})
|
|
cleanup()
|
|
cleanupLockManager()
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
process.on('uncaughtException', (error) => {
|
|
console.error('Uncaught Exception:', error)
|
|
logger.error(
|
|
`Uncaught Exception: ${error instanceof Error ? `${error.message}\n${error.stack ?? ''}` : String(error)}`
|
|
)
|
|
process.exit(1)
|
|
})
|
|
|
|
process.on('unhandledRejection', (reason) => {
|
|
console.error('Unhandled Rejection:', reason)
|
|
logger.error(
|
|
`Unhandled Rejection: ${reason instanceof Error ? `${reason.message}\n${reason.stack ?? ''}` : String(reason)}`
|
|
)
|
|
process.exit(1)
|
|
})
|
|
|
|
new MainProcess()
|