Files
ChatLab/electron/main/index.ts

312 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app, shell, BrowserWindow, protocol, nativeTheme } from 'electron'
import { join } from 'path'
import { optimizer, is, platform } from '@electron-toolkit/utils'
import { checkUpdate } from './update'
import mainIpcMain, { cleanup } from './ipcMain'
import { initAnalytics, trackDailyActive } from './analytics'
import { initProxy } from './network/proxy'
import { needsLegacyMigration, migrateFromLegacyDir, ensureAppDirs, cleanupPendingDeleteDir } from './paths'
import { migrateAllDatabases, checkMigrationNeeded } from './database/core'
import { initLocale } from './i18n'
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
// 统一通过扩展类型访问退出标记,避免使用 @ts-ignore。
const appWithQuitFlag = app as AppWithQuitFlag
class MainProcess {
mainWindow: BrowserWindow | null
constructor() {
// 主窗口
this.mainWindow = null
// 设置应用程序名称
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
// 初始化
this.checkApp().then(async (lockObtained) => {
if (lockObtained) {
await this.init()
}
})
}
// 单例锁
async checkApp() {
if (!app.requestSingleInstanceLock()) {
app.quit()
// 未获得锁
return false
}
// 聚焦到当前程序
else {
app.on('second-instance', () => {
if (this.mainWindow) {
this.mainWindow.show()
if (this.mainWindow.isMinimized()) this.mainWindow.restore()
this.mainWindow.focus()
}
})
// 获得锁
return true
}
}
// 初始化程序
async init() {
initAnalytics()
// 清理上次切换目录后的旧数据目录
cleanupPendingDeleteDir()
// 执行数据目录迁移(从 Documents/ChatLab 迁移到 userData
this.migrateDataIfNeeded()
// 确保应用目录存在
ensureAppDirs()
// 初始化主进程国际化(在 ensureAppDirs 之后,确保 settings 目录存在)
await initLocale()
// 执行数据库 schema 迁移(确保所有数据库在 Worker 查询前已是最新 schema
this.migrateDatabasesIfNeeded()
initProxy() // 初始化代理配置
// 暂不注册自定义协议,避免触发系统 URL 协议关联提示
// 应用程序准备好之前注册
protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }])
// 主应用程序事件
this.mainAppEvents()
}
// 从旧目录迁移数据(静默迁移)
migrateDataIfNeeded() {
if (needsLegacyMigration()) {
console.log('[Main] Legacy data migration needed, starting migration...')
const result = migrateFromLegacyDir()
if (result.success) {
console.log(`[Main] Migration completed. Migrated: ${result.migratedDirs.join(', ')}`)
} else {
console.error('[Main] Migration failed:', result.error)
}
} else {
console.log('[Main] No legacy data migration needed')
}
}
// 执行数据库 schema 迁移(静默迁移)
migrateDatabasesIfNeeded() {
try {
const { count } = checkMigrationNeeded()
if (count > 0) {
const result = migrateAllDatabases()
if (!result.success) {
console.error('[Main] Database schema migration failed:', result.error)
}
}
} catch (error) {
console.error('[Main] Error in migrateDatabasesIfNeeded:', error)
}
}
// 创建主窗口
async createWindow() {
// 平台差异化窗口配置
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1180,
height: 752,
minWidth: 1180,
minHeight: 752,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
devTools: true,
},
}
// macOS: 使用 hiddenInset 保留红绿灯按钮
// Windows: 使用 titleBarOverlay在自定义标题栏区域右侧显示原生窗口按钮
// Linux: 使用自定义标题栏和自定义按钮
if (platform.isMacOS) {
windowOptions.titleBarStyle = 'hiddenInset'
} else if (platform.isWindows) {
// 保留系统框架,只隐藏标题栏内容,把内容区域顶到最上方
windowOptions.titleBarStyle = 'hidden'
// 获取当前主题状态
const isDark = nativeTheme.shouldUseDarkColors
windowOptions.titleBarOverlay = {
// 背景色与应用背景匹配,确保 hover 效果正确
color: isDark ? '#111827' : '#f9fafb', // dark: gray-900, light: gray-50
// 图标颜色适配主题
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
height: 32,
}
} else {
// Linux 继续使用无边框 + 自定义按钮
windowOptions.frame = false
}
this.mainWindow = new BrowserWindow(windowOptions)
this.mainWindow.once('ready-to-show', () => {
this.mainWindow?.show()
// Windows 上根据当前主题设置 titleBarOverlay 颜色
if (platform.isWindows) {
const isDark = nativeTheme.shouldUseDarkColors
this.mainWindow?.setTitleBarOverlay({
color: isDark ? '#111827' : '#f9fafb', // dark: gray-900, light: gray-50
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
height: 32,
})
// 监听主题变化,动态更新颜色
nativeTheme.on('updated', () => {
if (this.mainWindow && platform.isWindows) {
const isDark = nativeTheme.shouldUseDarkColors
this.mainWindow.setTitleBarOverlay({
color: isDark ? '#111827' : '#f9fafb', // dark: gray-900, light: gray-50
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
height: 32,
})
}
})
}
})
// 主窗口事件
this.mainWindowEvents()
this.mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
this.mainWindow.loadFile(join(__dirname, '../../out/renderer/index.html'))
}
}
// 主应用程序事件
mainAppEvents() {
app.whenReady().then(async () => {
console.log('[Main] App is ready')
// 设置Windows应用程序用户模型id
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
// 记录日活(用于统计操作系统版本、客户端版本,便于更好的适配客户端)
trackDailyActive()
// 创建主窗口
console.log('[Main] Creating window...')
await this.createWindow()
console.log('[Main] Window created')
// 检查更新逻辑
checkUpdate(this.mainWindow)
// 引入主进程ipcMain
if (this.mainWindow) {
console.log('[Main] Registering IPC handlers...')
mainIpcMain(this.mainWindow)
console.log('[Main] IPC handlers registered')
}
// 开发环境下 F12 打开控制台
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
app.on('activate', () => {
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
if (BrowserWindow.getAllWindows().length === 0) {
this.createWindow()
return
}
if (platform.isMacOS) {
this.mainWindow?.show()
}
})
// 监听渲染进程崩溃
app.on('render-process-gone', (_event, w, d) => {
if (d.reason == 'crashed') {
w.reload()
}
// fs.appendFile(`./error-log-${+new Date()}.txt`, `${new Date()}渲染进程被杀死${d.reason}\n`)
})
// 自定义协议
app.on('open-url', (_, url) => {
console.log('Received custom protocol URL:', url)
})
// 当所有窗口都关闭时退出应用macOS 除外
app.on('window-all-closed', () => {
if (!platform.isMacOS) {
app.quit()
}
})
// 只有显式调用quit才退出系统区分MAC系统程序坞退出和点击X隐藏
app.on('before-quit', () => {
appWithQuitFlag.isQuiting = true
})
// 退出前清理资源
app.on('will-quit', () => {
cleanup()
})
})
}
// 主窗口事件
mainWindowEvents() {
if (!this.mainWindow) {
return
}
this.mainWindow.webContents.on('did-finish-load', () => {
setTimeout(() => {
if (this.mainWindow) {
this.mainWindow.webContents.send('app-started')
}
}, 500)
})
this.mainWindow.on('maximize', () => {
this.mainWindow?.webContents.send('windowState', true)
})
this.mainWindow.on('unmaximize', () => {
this.mainWindow?.webContents.send('windowState', false)
})
// 窗口关闭
this.mainWindow.on('close', (event) => {
if (platform.isMacOS) {
// macOS: 只有明确退出时才真正关闭,否则只隐藏窗口(符合 macOS 用户习惯)
if (!appWithQuitFlag.isQuiting) {
event.preventDefault()
this.mainWindow?.hide()
}
}
// Windows/Linux: 不阻止关闭,正常触发 window-all-closed → app.quit() → cleanup()
})
}
}
// 捕获未捕获的异常
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error)
})
new MainProcess()