Files
AUTO-MAS-test/frontend/electron/main.ts

1064 lines
28 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,
BrowserWindow,
ipcMain,
dialog,
shell,
Tray,
Menu,
nativeImage,
screen,
} from 'electron'
import * as path from 'path'
import * as fs from 'fs'
import { spawn } from 'child_process'
import { getAppRoot, checkEnvironment } from './services/environmentService'
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
import {
setMainWindow as setPythonMainWindow,
downloadPython,
installPipPackage,
installDependencies,
startBackend,
stopBackend,
} from './services/pythonService'
import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService'
import { setupLogger, log, getLogPath, getLogFiles, cleanOldLogs } from './services/logService'
// 检查是否以管理员权限运行
function isRunningAsAdmin(): boolean {
try {
// 在Windows上尝试写入系统目录来检查管理员权限
if (process.platform === 'win32') {
const testPath = path.join(process.env.WINDIR || 'C:\\Windows', 'temp', 'admin-test.tmp')
try {
fs.writeFileSync(testPath, 'test')
fs.unlinkSync(testPath)
return true
} catch {
return false
}
}
return true // 非Windows系统暂时返回true
} catch {
return false
}
}
// 重新以管理员权限启动应用
function restartAsAdmin(): void {
if (process.platform === 'win32') {
const exePath = process.execPath
const args = process.argv.slice(1)
// 使用PowerShell以管理员权限启动
spawn(
'powershell',
[
'-Command',
`Start-Process -FilePath "${exePath}" -ArgumentList "${args.join(' ')}" -Verb RunAs`,
],
{
detached: true,
stdio: 'ignore',
}
)
app.quit()
}
}
let tray: Tray | null = null
let isQuitting = false
let saveWindowStateTimeout: NodeJS.Timeout | null = null
// 配置接口
interface AppConfig {
UI: {
IfShowTray: boolean
IfToTray: boolean
location: string
maximized: boolean
size: string
}
Start: {
IfMinimizeDirectly: boolean
IfSelfStart: boolean
}
[key: string]: any
}
// 默认配置
const defaultConfig: AppConfig = {
UI: {
IfShowTray: false,
IfToTray: false,
location: '100,100',
maximized: false,
size: '1600,1000',
},
Start: {
IfMinimizeDirectly: false,
IfSelfStart: false,
},
}
// 加载配置
function loadConfig(): AppConfig {
try {
const appRoot = getAppRoot()
const configPath = path.join(appRoot, 'config', 'frontend_config.json')
if (fs.existsSync(configPath)) {
const configData = fs.readFileSync(configPath, 'utf8')
const config = JSON.parse(configData)
return { ...defaultConfig, ...config }
}
} catch (error) {
log.error('加载配置失败:', error)
}
return defaultConfig
}
// 保存配置
function saveConfig(config: AppConfig) {
try {
const appRoot = getAppRoot()
const configDir = path.join(appRoot, 'config')
const configPath = path.join(configDir, 'frontend_config.json')
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true })
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
} catch (error) {
log.error('保存配置失败:', error)
}
}
// 创建托盘
function createTray() {
if (tray) return
// 尝试多个可能的图标路径
const iconPaths = [
path.join(__dirname, '../public/AUTO-MAS.ico'),
path.join(process.resourcesPath, 'assets/AUTO-MAS.ico'),
path.join(app.getAppPath(), 'public/AUTO-MAS.ico'),
path.join(app.getAppPath(), 'dist/AUTO-MAS.ico'),
]
let trayIcon
try {
// 尝试加载图标
for (const iconPath of iconPaths) {
if (fs.existsSync(iconPath)) {
trayIcon = nativeImage.createFromPath(iconPath)
if (!trayIcon.isEmpty()) {
log.info(`成功加载托盘图标: ${iconPath}`)
break
}
}
}
// 如果所有路径都失败,创建一个默认图标
if (!trayIcon || trayIcon.isEmpty()) {
log.warn('无法加载托盘图标,使用默认图标')
trayIcon = nativeImage.createEmpty()
}
} catch (error) {
log.error('加载托盘图标失败:', error)
trayIcon = nativeImage.createEmpty()
}
tray = new Tray(trayIcon)
const contextMenu = Menu.buildFromTemplate([
{
label: '显示窗口',
click: () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.setSkipTaskbar(false) // 恢复任务栏图标
mainWindow.show()
mainWindow.focus()
}
},
},
{
label: '隐藏窗口',
click: () => {
if (mainWindow) {
const currentConfig = loadConfig()
if (currentConfig.UI.IfToTray) {
mainWindow.setSkipTaskbar(true) // 隐藏任务栏图标
}
mainWindow.hide()
}
},
},
{ type: 'separator' },
{
label: '退出',
click: () => {
isQuitting = true
app.quit()
},
},
])
tray.setContextMenu(contextMenu)
tray.setToolTip('AUTO-MAS')
// 双击托盘图标显示/隐藏窗口
tray.on('double-click', () => {
if (mainWindow) {
const currentConfig = loadConfig()
if (mainWindow.isVisible()) {
if (currentConfig.UI.IfToTray) {
mainWindow.setSkipTaskbar(true) // 隐藏任务栏图标
}
mainWindow.hide()
} else {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.setSkipTaskbar(false) // 恢复任务栏图标
mainWindow.show()
mainWindow.focus()
}
}
})
}
// 销毁托盘
function destroyTray() {
if (tray) {
tray.destroy()
tray = null
}
}
// 更新托盘状态
function updateTrayVisibility(config: AppConfig) {
// 根据需求逻辑判断是否应该显示托盘
let shouldShowTray = false
if (config.UI.IfShowTray && config.UI.IfToTray) {
// 勾选常驻显示托盘和最小化到托盘,就一直展示托盘
shouldShowTray = true
} else if (config.UI.IfShowTray && !config.UI.IfToTray) {
// 勾选常驻显示托盘但没有最小化到托盘,就一直展示托盘
shouldShowTray = true
} else if (!config.UI.IfShowTray && config.UI.IfToTray) {
// 没有常驻显示托盘但勾选最小化到托盘,有窗口时就只有窗口,最小化后任务栏消失,只有托盘
shouldShowTray = !mainWindow || !mainWindow.isVisible()
} else {
// 没有常驻显示托盘也没有最小化到托盘,托盘一直不展示
shouldShowTray = false
}
// 特殊情况:如果没有窗口显示且没有托盘,强制显示托盘避免程序成为幽灵
if (!shouldShowTray && (!mainWindow || !mainWindow.isVisible()) && !tray) {
shouldShowTray = true
log.warn('防幽灵机制:强制显示托盘图标')
}
if (shouldShowTray && !tray) {
createTray()
log.info('托盘图标已创建')
} else if (!shouldShowTray && tray) {
destroyTray()
log.info('托盘图标已销毁')
}
}
let mainWindow: Electron.BrowserWindow | null = null
function createWindow() {
log.info('开始创建主窗口')
const config = loadConfig()
// 解析配置
const [cfgW, cfgH] = config.UI.size.split(',').map((s: string) => parseInt(s.trim(), 10) || 1600)
const [cfgX, cfgY] = config.UI.location
.split(',')
.map((s: string) => parseInt(s.trim(), 10) || 100)
// 以目标位置选最近显示器
const targetDisplay = screen.getDisplayNearestPoint({ x: cfgX, y: cfgY })
const sf = targetDisplay.scaleFactor
// 逻辑最小尺寸DIP
const minDipW = Math.floor(1600 / sf)
const minDipH = Math.floor(900 / sf)
// 初始窗口逻辑尺寸DIP
let initW = Math.max(cfgW, minDipW)
let initH = Math.max(cfgH, minDipH)
// 不超过工作区
const { width: waW, height: waH } = targetDisplay.workAreaSize
initW = Math.min(initW, waW)
initH = Math.min(initH, waH)
// 关键:用局部常量 win全程用它类型不为 null
const win = new BrowserWindow({
x: cfgX,
y: cfgY,
width: initW,
height: initH,
minWidth: minDipW,
minHeight: minDipH,
useContentSize: true,
frame: false,
titleBarStyle: 'hidden',
icon: path.join(__dirname, '../public/AUTO-MAS.ico'),
autoHideMenuBar: true,
show: !config.Start.IfMinimizeDirectly,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
},
})
// 把局部的 win 赋值给模块级(供其他模块/函数用)
mainWindow = win
// 根据显示器动态更新最小尺寸/边界
const recomputeMinSize = () => {
// 这里用 win不会是 null
const bounds = win.getBounds()
const disp = screen.getDisplayMatching(bounds)
const s = disp.scaleFactor
const w = Math.floor(1600 / s)
const h = Math.floor(900 / s)
const [curMinW, curMinH] = win.getMinimumSize()
if (w !== curMinW || h !== curMinH) {
win.setMinimumSize(w, h)
if (win.isMaximized()) return
const { width: wW, height: wH } = disp.workAreaSize
const newBounds = { ...bounds }
if (newBounds.width > wW) newBounds.width = wW
if (newBounds.height > wH) newBounds.height = wH
if (newBounds.width < w) newBounds.width = w
if (newBounds.height < h) newBounds.height = h
win.setBounds(newBounds)
}
}
// 监听显示器变化/窗口移动
win.on('moved', recomputeMinSize)
win.on('resized', recomputeMinSize)
screen.on('display-metrics-changed', recomputeMinSize)
// 最大化配置
if (config.UI.maximized) {
win.maximize()
}
win.setMenuBarVisibility(false)
const devServer = process.env.VITE_DEV_SERVER_URL
if (devServer) {
log.info(`加载开发服务器: ${devServer}`)
win.loadURL(devServer)
} else {
const indexHtmlPath = path.join(app.getAppPath(), 'dist', 'index.html')
log.info(`加载生产环境页面: ${indexHtmlPath}`)
win.loadFile(indexHtmlPath)
}
// 窗口事件处理
win.on('close', (event: Electron.Event) => {
const currentConfig = loadConfig()
if (!isQuitting && currentConfig.UI.IfToTray) {
event.preventDefault()
win.hide()
win.setSkipTaskbar(true)
updateTrayVisibility(currentConfig)
log.info('窗口已最小化到托盘,任务栏图标已隐藏')
} else {
saveWindowState()
}
})
win.on('closed', () => {
log.info('主窗口已关闭')
// 清理监听(可选)
screen.removeListener('display-metrics-changed', recomputeMinSize)
// 置空模块级引用
mainWindow = null
})
win.on('minimize', () => {
const currentConfig = loadConfig()
if (currentConfig.UI.IfToTray) {
win.hide()
win.setSkipTaskbar(true)
updateTrayVisibility(currentConfig)
log.info('窗口已最小化到托盘,任务栏图标已隐藏')
}
})
win.on('show', () => {
const currentConfig = loadConfig()
win.setSkipTaskbar(false)
updateTrayVisibility(currentConfig)
log.info('窗口已显示,任务栏图标已恢复')
})
win.on('hide', () => {
const currentConfig = loadConfig()
if (currentConfig.UI.IfToTray) {
win.setSkipTaskbar(true)
log.info('窗口已隐藏,任务栏图标已隐藏')
}
updateTrayVisibility(currentConfig)
})
// 移动/调整大小/最大化状态变化时保存
win.on('moved', saveWindowState)
win.on('resized', saveWindowState)
win.on('maximize', saveWindowState)
win.on('unmaximize', saveWindowState)
// 设置各个服务的主窗口引用(此处 win 一定存在,可直接传)
setDownloadMainWindow(win)
setPythonMainWindow(win)
setGitMainWindow(win)
log.info('主窗口创建完成,服务引用已设置')
// 根据配置初始化托盘
updateTrayVisibility(config)
}
// 保存窗口状态(带防抖)
function saveWindowState() {
if (!mainWindow) return
// 清除之前的定时器
if (saveWindowStateTimeout) {
clearTimeout(saveWindowStateTimeout)
}
// 设置新的定时器500ms后保存
saveWindowStateTimeout = setTimeout(() => {
try {
const config = loadConfig()
const bounds = mainWindow!.getBounds()
const isMaximized = mainWindow!.isMaximized()
// 只有在窗口不是最大化状态时才保存位置和大小
if (!isMaximized) {
config.UI.size = `${bounds.width},${bounds.height}`
config.UI.location = `${bounds.x},${bounds.y}`
}
config.UI.maximized = isMaximized
saveConfig(config)
log.info('窗口状态已保存')
} catch (error) {
log.error('保存窗口状态失败:', error)
}
}, 500)
}
// IPC处理函数
ipcMain.handle('open-dev-tools', () => {
if (mainWindow) {
mainWindow.webContents.openDevTools({ mode: 'undocked' })
}
})
// 窗口控制
ipcMain.handle('window-minimize', () => {
if (mainWindow) {
mainWindow.minimize()
}
})
ipcMain.handle('window-maximize', () => {
if (mainWindow) {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize()
} else {
mainWindow.maximize()
}
}
})
ipcMain.handle('window-close', () => {
if (mainWindow) {
mainWindow.close()
}
})
ipcMain.handle('window-is-maximized', () => {
return mainWindow ? mainWindow.isMaximized() : false
})
ipcMain.handle('select-folder', async () => {
if (!mainWindow) return null
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'],
title: '选择文件夹',
})
return result.canceled ? null : result.filePaths[0]
})
ipcMain.handle('select-file', async (event, filters = []) => {
if (!mainWindow) return []
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
title: '选择文件',
filters: filters.length > 0 ? filters : [{ name: '所有文件', extensions: ['*'] }],
})
return result.canceled ? [] : result.filePaths
})
// 在系统默认浏览器中打开URL
ipcMain.handle('open-url', async (_event, url: string) => {
try {
await shell.openExternal(url)
return { success: true }
} catch (error) {
if (error instanceof Error) {
console.error('打开链接失败:', error.message)
return { success: false, error: error.message }
} else {
console.error('未知错误:', error)
return { success: false, error: String(error) }
}
}
})
// 打开文件
ipcMain.handle('open-file', async (_event, filePath: string) => {
try {
await shell.openPath(filePath)
} catch (error) {
console.error('打开文件失败:', error)
throw error
}
})
// 显示文件所在目录并选中文件
ipcMain.handle('show-item-in-folder', async (_event, filePath: string) => {
try {
shell.showItemInFolder(filePath)
} catch (error) {
console.error('显示文件所在目录失败:', error)
throw error
}
})
// 环境检查
ipcMain.handle('check-environment', async () => {
const appRoot = getAppRoot()
return checkEnvironment(appRoot)
})
// 关键文件检查 - 每次都重新检查exe文件是否存在
ipcMain.handle('check-critical-files', async () => {
try {
const appRoot = getAppRoot()
// 检查Python可执行文件
const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe')
const pythonExists = fs.existsSync(pythonPath)
// 检查pip通常与Python一起安装
const pipPath = path.join(appRoot, 'environment', 'python', 'Scripts', 'pip.exe')
const pipExists = fs.existsSync(pipPath)
// 检查Git可执行文件
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
const gitExists = fs.existsSync(gitPath)
// 检查后端主文件
const mainPyPath = path.join(appRoot, 'main.py')
const mainPyExists = fs.existsSync(mainPyPath)
const result = {
pythonExists,
pipExists,
gitExists,
mainPyExists,
}
log.info('关键文件检查结果:', result)
return result
} catch (error) {
log.error('检查关键文件失败:', error)
return {
pythonExists: false,
pipExists: false,
gitExists: false,
mainPyExists: false,
}
}
})
// Python相关
ipcMain.handle('download-python', async (_event, mirror = 'tsinghua') => {
const appRoot = getAppRoot()
return downloadPython(appRoot, mirror)
})
ipcMain.handle('install-pip', async () => {
const appRoot = getAppRoot()
return installPipPackage(appRoot)
})
ipcMain.handle('install-dependencies', async (_event, mirror = 'tsinghua') => {
const appRoot = getAppRoot()
return installDependencies(appRoot, mirror)
})
ipcMain.handle('start-backend', async () => {
const appRoot = getAppRoot()
return startBackend(appRoot)
})
ipcMain.handle('stop-backend', async () => {
return stopBackend()
})
// Git相关
ipcMain.handle('download-git', async () => {
const appRoot = getAppRoot()
return downloadGit(appRoot)
})
ipcMain.handle('check-git-update', async () => {
try {
const appRoot = getAppRoot()
// 检查是否为Git仓库
const gitDir = path.join(appRoot, '.git')
if (!fs.existsSync(gitDir)) {
log.info('不是Git仓库跳过更新检查')
return { hasUpdate: false }
}
// 检查Git可执行文件是否存在
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
if (!fs.existsSync(gitPath)) {
log.warn('Git可执行文件不存在无法检查更新')
return { hasUpdate: false, error: 'Git可执行文件不存在' }
}
// 获取Git环境变量
const gitEnv = {
...process.env,
PATH: `${path.join(appRoot, 'environment', 'git', 'bin')};${path.join(appRoot, 'environment', 'git', 'mingw64', 'bin')};${process.env.PATH}`,
GIT_EXEC_PATH: path.join(appRoot, 'environment', 'git', 'mingw64', 'libexec', 'git-core'),
HOME: process.env.USERPROFILE || process.env.HOME,
GIT_CONFIG_NOSYSTEM: '1',
GIT_TERMINAL_PROMPT: '0',
GIT_ASKPASS: '',
}
log.info('开始检查Git仓库更新...')
// 执行 git fetch 获取最新的远程信息
await new Promise<void>((resolve, reject) => {
const fetchProc = spawn(gitPath, ['fetch', 'origin'], {
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
})
fetchProc.stdout?.on('data', data => {
log.info('git fetch output:', data.toString())
})
fetchProc.stderr?.on('data', data => {
log.info('git fetch stderr:', data.toString())
})
fetchProc.on('close', code => {
if (code === 0) {
resolve()
} else {
reject(new Error(`git fetch失败退出码: ${code}`))
}
})
fetchProc.on('error', reject)
})
// 检查本地分支是否落后于远程分支
const hasUpdate = await new Promise<boolean>((resolve, reject) => {
const statusProc = spawn(gitPath, ['status', '-uno', '--porcelain=v1'], {
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
})
let output = ''
statusProc.stdout?.on('data', data => {
output += data.toString()
})
statusProc.stderr?.on('data', data => {
log.info('git status stderr:', data.toString())
})
statusProc.on('close', code => {
if (code === 0) {
// 检查是否有 "Your branch is behind" 的信息
// 使用 git rev-list 来比较本地和远程分支
const revListProc = spawn(
gitPath,
['rev-list', '--count', 'HEAD..origin/feature/refactor'],
{
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
}
)
let revOutput = ''
revListProc.stdout?.on('data', data => {
revOutput += data.toString()
})
revListProc.on('close', revCode => {
if (revCode === 0) {
const commitsBehind = parseInt(revOutput.trim())
const hasUpdates = commitsBehind > 0
log.info(`本地分支落后远程分支 ${commitsBehind} 个提交hasUpdate: ${hasUpdates}`)
resolve(hasUpdates)
} else {
log.warn('无法比较本地和远程分支,假设有更新')
resolve(true) // 如果无法确定,假设有更新
}
})
revListProc.on('error', () => {
log.warn('git rev-list执行失败假设有更新')
resolve(true)
})
} else {
reject(new Error(`git status失败退出码: ${code}`))
}
})
statusProc.on('error', reject)
})
log.info(`Git更新检查完成hasUpdate: ${hasUpdate}`)
return { hasUpdate }
} catch (error) {
log.error('检查Git更新失败:', error)
// 如果检查失败返回true以触发更新流程确保代码是最新的
return { hasUpdate: true, error: error instanceof Error ? error.message : String(error) }
}
})
ipcMain.handle(
'clone-backend',
async (_event, repoUrl = 'https://github.com/AUTO-MAS-Project/AUTO-MAS.git') => {
const appRoot = getAppRoot()
return cloneBackend(appRoot, repoUrl)
}
)
ipcMain.handle(
'update-backend',
async (_event, repoUrl = 'https://github.com/AUTO-MAS-Project/AUTO-MAS.git') => {
const appRoot = getAppRoot()
return cloneBackend(appRoot, repoUrl) // 使用相同的逻辑会自动判断是pull还是clone
}
)
// 配置文件操作
ipcMain.handle('save-config', async (_event, config) => {
try {
const appRoot = getAppRoot()
const configDir = path.join(appRoot, 'config')
const configPath = path.join(configDir, 'frontend_config.json')
// 确保config目录存在
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true })
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
console.log(`配置已保存到: ${configPath}`)
// 如果是UI配置更新需要更新托盘状态
if (config.UI) {
updateTrayVisibility(config)
}
} catch (error) {
console.error('保存配置文件失败:', error)
throw error
}
})
// 新增实时更新托盘状态的IPC处理器
ipcMain.handle('update-tray-settings', async (_event, uiSettings) => {
try {
// 先更新配置文件
const currentConfig = loadConfig()
currentConfig.UI = { ...currentConfig.UI, ...uiSettings }
saveConfig(currentConfig)
// 立即更新托盘状态
updateTrayVisibility(currentConfig)
log.info('托盘设置已更新:', uiSettings)
return true
} catch (error) {
log.error('更新托盘设置失败:', error)
throw error
}
})
ipcMain.handle('load-config', async () => {
try {
const appRoot = getAppRoot()
const configPath = path.join(appRoot, 'config', 'frontend_config.json')
if (fs.existsSync(configPath)) {
const config = fs.readFileSync(configPath, 'utf8')
console.log(`从文件加载配置: ${configPath}`)
return JSON.parse(config)
}
return null
} catch (error) {
console.error('加载配置文件失败:', error)
return null
}
})
ipcMain.handle('reset-config', async () => {
try {
const appRoot = getAppRoot()
const configPath = path.join(appRoot, 'config', 'frontend_config.json')
if (fs.existsSync(configPath)) {
fs.unlinkSync(configPath)
console.log(`配置文件已删除: ${configPath}`)
}
} catch (error) {
console.error('重置配置文件失败:', error)
throw error
}
})
// 日志文件操作
ipcMain.handle('get-log-path', async () => {
try {
return getLogPath()
} catch (error) {
log.error('获取日志路径失败:', error)
throw error
}
})
ipcMain.handle('get-log-files', async _event => {
try {
return getLogFiles()
} catch (error) {
log.error('获取日志文件列表失败:', error)
throw error
}
})
ipcMain.handle('get-logs', async (_event, lines?: number, fileName?: string) => {
try {
let logFilePath: string
if (fileName) {
// 如果指定了文件名,使用指定的文件
const appRoot = getAppRoot()
logFilePath = path.join(appRoot, 'logs', fileName)
} else {
// 否则使用当前日志文件
logFilePath = getLogPath()
}
if (!fs.existsSync(logFilePath)) {
return ''
}
const logs = fs.readFileSync(logFilePath, 'utf8')
if (lines && lines > 0) {
const logLines = logs.split('\n')
return logLines.slice(-lines).join('\n')
}
return logs
} catch (error) {
log.error('读取日志文件失败:', error)
throw error
}
})
ipcMain.handle('clear-logs', async (_event, fileName?: string) => {
try {
let logFilePath: string
if (fileName) {
// 如果指定了文件名,清空指定的文件
const appRoot = getAppRoot()
logFilePath = path.join(appRoot, 'logs', fileName)
} else {
// 否则清空当前日志文件
logFilePath = getLogPath()
}
if (fs.existsSync(logFilePath)) {
fs.writeFileSync(logFilePath, '', 'utf8')
log.info(`日志文件已清空: ${fileName || '当前文件'}`)
}
} catch (error) {
log.error('清空日志文件失败:', error)
throw error
}
})
ipcMain.handle('clean-old-logs', async (_event, daysToKeep = 7) => {
try {
cleanOldLogs(daysToKeep)
log.info(`已清理${daysToKeep}天前的旧日志文件`)
} catch (error) {
log.error('清理旧日志文件失败:', error)
throw error
}
})
// 保留原有的日志操作方法以兼容现有代码
ipcMain.handle('save-logs-to-file', async (_event, logs: string) => {
try {
const appRoot = getAppRoot()
const logsDir = path.join(appRoot, 'logs')
// 确保logs目录存在
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true })
}
const logFilePath = path.join(logsDir, 'app.log')
fs.writeFileSync(logFilePath, logs, 'utf8')
log.info(`日志已保存到: ${logFilePath}`)
} catch (error) {
log.error('保存日志文件失败:', error)
throw error
}
})
ipcMain.handle('load-logs-from-file', async () => {
try {
const appRoot = getAppRoot()
const logFilePath = path.join(appRoot, 'logs', 'app.log')
if (fs.existsSync(logFilePath)) {
const logs = fs.readFileSync(logFilePath, 'utf8')
log.info(`从文件加载日志: ${logFilePath}`)
return logs
}
return null
} catch (error) {
log.error('加载日志文件失败:', error)
return null
}
})
// 管理员权限相关
ipcMain.handle('check-admin', () => {
return isRunningAsAdmin()
})
ipcMain.handle('restart-as-admin', () => {
restartAsAdmin()
})
// 应用生命周期
// 保证应用单例运行
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
process.exit(0)
}
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
}
})
app.on('before-quit', async event => {
// 只处理一次,避免多重触发
if (!isQuitting) {
event.preventDefault()
isQuitting = true
log.info('应用准备退出')
// 清理托盘
destroyTray()
try {
await stopBackend()
log.info('后端服务已停止')
} catch (e) {
log.error('停止后端时出错:', e)
console.error('停止后端时出错:', e)
} finally {
log.info('应用退出')
app.exit(0)
}
}
})
app.whenReady().then(() => {
// 初始化日志系统
setupLogger()
// 清理7天前的旧日志
cleanOldLogs(7)
log.info('应用启动')
log.info(`应用版本: ${app.getVersion()}`)
log.info(`Electron版本: ${process.versions.electron}`)
log.info(`Node版本: ${process.versions.node}`)
log.info(`平台: ${process.platform}`)
// 检查管理员权限
if (!isRunningAsAdmin()) {
log.warn('应用未以管理员权限运行')
console.log('应用未以管理员权限运行')
// 在生产环境中,可以选择是否强制要求管理员权限
// 这里先创建窗口,让用户选择是否重新启动
} else {
log.info('应用以管理员权限运行')
}
createWindow()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', () => {
if (mainWindow === null) createWindow()
})