Files
AUTO-MAS-test/frontend/electron/main.ts
MoeSnowyFox 1d91204842 feat(electron): 添加系统级对话框功能
- 在 ElectronAPI 中新增对话框相关方法:showQuestionDialog、dialogResponse、resizeDialogWindow
- 实现独立的对话框窗口 dialog.html,支持自定义标题、消息和选项- 添加对话框窗口的键盘导航和焦点管理功能
- 在主进程中实现对话框窗口的创建、显示和响应处理
- 更新 WebSocket 消息监听器组件,使用系统级对话框替代应用内弹窗- 优化进程清理逻辑,增加多种清理方法并行执行
- 重构日志工具类,改进 Electron API 调用方式
- 调整 Git 更新检查逻辑,避免直接访问 GitHub
- 移除冗余的类型定义文件,统一 Electron API 接口定义
2025-10-01 23:24:39 +08:00

1309 lines
35 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,
dialog,
ipcMain,
Menu,
nativeImage,
screen,
shell,
Tray,
} from 'electron'
import * as path from 'path'
import * as fs from 'fs'
import { exec, spawn } from 'child_process'
import { checkEnvironment, getAppRoot } from './services/environmentService'
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
import {
downloadPython,
installDependencies,
installPipPackage,
setMainWindow as setPythonMainWindow,
startBackend,
} from './services/pythonService'
import { cloneBackend, downloadGit, setMainWindow as setGitMainWindow } from './services/gitService'
import { cleanOldLogs, getLogFiles, getLogPath, log, setupLogger } from './services/logService'
// 强制清理相关进程的函数
async function forceKillRelatedProcesses(): Promise<void> {
try {
const { killAllRelatedProcesses } = await import('./utils/processManager')
await killAllRelatedProcesses()
log.info('所有相关进程已清理')
} catch (error) {
log.error('清理进程时出错:', error)
// 备用清理方法
if (process.platform === 'win32') {
const appRoot = getAppRoot()
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
return new Promise(resolve => {
// 使用更简单的命令强制结束相关进程
exec(`taskkill /f /im python.exe`, error => {
if (error) {
log.warn('备用清理方法失败:', error.message)
} else {
log.info('备用清理方法执行成功')
}
resolve()
})
})
}
}
}
// 检查是否以管理员权限运行
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 {
// 立即保存窗口状态,不使用防抖
if (!win.isDestroyed()) {
try {
const config = loadConfig()
const bounds = win.getBounds()
const isMaximized = win.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)
}
}
}
})
win.on('closed', () => {
log.info('主窗口已关闭')
// 清理监听(可选)
screen.removeListener('display-metrics-changed', recomputeMinSize)
// 置空模块级引用
mainWindow = null
// 如果是正在退出,立即执行进程清理
if (isQuitting) {
log.info('窗口关闭,执行最终清理')
setTimeout(async () => {
try {
await forceKillRelatedProcesses()
} catch (e) {
log.error('最终清理失败:', e)
}
process.exit(0)
}, 100)
}
})
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 || mainWindow.isDestroyed()) return
// 清除之前的定时器
if (saveWindowStateTimeout) {
clearTimeout(saveWindowStateTimeout)
}
// 设置新的定时器500ms后保存
saveWindowStateTimeout = setTimeout(() => {
try {
// 再次检查窗口是否存在且未销毁
if (!mainWindow || mainWindow.isDestroyed()) {
log.warn('窗口已销毁,跳过保存状态')
return
}
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) {
isQuitting = true
mainWindow.close()
}
})
// 添加强制退出处理器
ipcMain.handle('app-quit', () => {
isQuitting = true
app.quit()
})
// 添加进程管理相关的 IPC 处理器
ipcMain.handle('get-related-processes', async () => {
try {
const { getRelatedProcesses } = await import('./utils/processManager')
return await getRelatedProcesses()
} catch (error) {
log.error('获取进程信息失败:', error)
return []
}
})
ipcMain.handle('kill-all-processes', async () => {
try {
await forceKillRelatedProcesses()
return { success: true }
} catch (error) {
log.error('强制清理进程失败:', error)
return { success: false, error: error instanceof Error ? error.message : String(error) }
}
})
// 添加一个测试用的强制退出命令
ipcMain.handle('force-exit', async () => {
log.info('收到强制退出命令')
isQuitting = true
// 立即清理进程
try {
await forceKillRelatedProcesses()
} catch (e) {
log.error('强制清理失败:', e)
}
// 强制退出
setTimeout(() => {
process.exit(0)
}, 500)
return { success: true }
})
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 () => {
const { stopBackend } = await import('./services/pythonService')
return stopBackend()
})
// 全局存储对话框窗口引用和回调
let dialogWindows = new Map<string, BrowserWindow>()
let dialogCallbacks = new Map<string, (result: boolean) => void>()
// 创建对话框窗口
function createQuestionDialog(questionData: any): Promise<boolean> {
return new Promise((resolve) => {
const messageId = questionData.messageId || 'dialog_' + Date.now()
// 存储回调函数
dialogCallbacks.set(messageId, resolve)
// 准备对话框数据
const dialogData = {
title: questionData.title || '操作确认',
message: questionData.message || '是否要执行此操作?',
options: questionData.options || ['确定', '取消'],
messageId: messageId
}
// 创建对话框窗口
const dialogWindow = new BrowserWindow({
width: 450,
height: 200,
minWidth: 350,
minHeight: 150,
maxWidth: 600,
maxHeight: 400,
resizable: true,
minimizable: false,
maximizable: false,
alwaysOnTop: true,
show: false,
frame: false,
modal: mainWindow ? true : false,
parent: mainWindow || undefined,
icon: path.join(__dirname, '../public/AUTO-MAS.ico'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
})
// 存储窗口引用
dialogWindows.set(messageId, dialogWindow)
// 编码对话框数据
const encodedData = encodeURIComponent(JSON.stringify(dialogData))
// 加载对话框页面
const dialogUrl = `file://${path.join(__dirname, '../public/dialog.html')}?data=${encodedData}`
dialogWindow.loadURL(dialogUrl)
// 窗口准备好后显示并居中
dialogWindow.once('ready-to-show', () => {
// 计算居中位置
if (mainWindow && !mainWindow.isDestroyed()) {
const mainBounds = mainWindow.getBounds()
const dialogBounds = dialogWindow.getBounds()
const x = Math.round(mainBounds.x + (mainBounds.width - dialogBounds.width) / 2)
const y = Math.round(mainBounds.y + (mainBounds.height - dialogBounds.height) / 2)
dialogWindow.setPosition(x, y)
} else {
dialogWindow.center()
}
dialogWindow.show()
dialogWindow.focus()
})
// 窗口关闭时清理
dialogWindow.on('closed', () => {
dialogWindows.delete(messageId)
const callback = dialogCallbacks.get(messageId)
if (callback) {
dialogCallbacks.delete(messageId)
callback(false) // 默认返回 false (取消)
}
})
log.info(`对话框窗口已创建: ${messageId}`)
})
}
// 显示问题对话框
ipcMain.handle('show-question-dialog', async (_event, questionData) => {
log.info('收到显示对话框请求:', questionData)
try {
const result = await createQuestionDialog(questionData)
log.info(`对话框结果: ${result}`)
return result
} catch (error) {
log.error('创建对话框失败:', error)
return false
}
})
// 处理对话框响应
ipcMain.handle('dialog-response', async (_event, messageId: string, choice: boolean) => {
log.info(`收到对话框响应: ${messageId} = ${choice}`)
const callback = dialogCallbacks.get(messageId)
if (callback) {
dialogCallbacks.delete(messageId)
callback(choice)
}
// 关闭对话框窗口
const dialogWindow = dialogWindows.get(messageId)
if (dialogWindow && !dialogWindow.isDestroyed()) {
dialogWindow.close()
}
dialogWindows.delete(messageId)
return true
})
// 调整对话框窗口大小
ipcMain.handle('resize-dialog-window', async (_event, height: number) => {
// 获取当前活动的对话框窗口(最后创建的)
const dialogWindow = Array.from(dialogWindows.values()).pop()
if (dialogWindow && !dialogWindow.isDestroyed()) {
const bounds = dialogWindow.getBounds()
dialogWindow.setBounds({
...bounds,
height: Math.max(150, Math.min(400, height))
})
}
})
// 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仓库更新跳过fetch避免直接访问GitHub...')
// 不执行fetch直接检查本地状态
// 这样避免了直接访问GitHub而是在后续的pull操作中使用镜像站
// 获取当前HEAD的commit hash
const currentCommit = await new Promise<string>((resolve, reject) => {
const revParseProc = spawn(gitPath, ['rev-parse', 'HEAD'], {
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
})
let output = ''
revParseProc.stdout?.on('data', data => {
output += data.toString()
})
revParseProc.on('close', code => {
if (code === 0) {
resolve(output.trim())
} else {
reject(new Error(`git rev-parse失败退出码: ${code}`))
}
})
revParseProc.on('error', reject)
})
log.info(`当前本地commit: ${currentCommit}`)
// 由于我们跳过了fetch步骤避免直接访问GitHub
// 我们无法准确知道远程是否有更新
// 因此返回true让后续的pull操作通过镜像站来检查和获取更新
// 如果没有更新pull操作会很快完成且不会有实际变化
log.info('跳过远程检查返回hasUpdate=true以触发镜像站更新流程')
return { hasUpdate: true, skipReason: 'avoided_github_access' }
} 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('应用准备退出')
// 清理定时器
if (saveWindowStateTimeout) {
clearTimeout(saveWindowStateTimeout)
saveWindowStateTimeout = null
}
// 清理托盘
destroyTray()
// 立即开始强制清理,不等待优雅关闭
log.info('开始强制清理所有相关进程')
try {
// 并行执行多种清理方法
const cleanupPromises = [
// 方法1: 使用我们的进程管理器
forceKillRelatedProcesses(),
// 方法2: 直接使用 taskkill 命令
new Promise<void>(resolve => {
if (process.platform === 'win32') {
const appRoot = getAppRoot()
const commands = [
`taskkill /f /im python.exe`,
`wmic process where "CommandLine like '%main.py%'" delete`,
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`,
]
let completed = 0
commands.forEach(cmd => {
exec(cmd, () => {
completed++
if (completed === commands.length) {
resolve()
}
})
})
// 2秒超时
setTimeout(resolve, 2000)
} else {
resolve()
}
}),
]
// 最多等待3秒
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000))
await Promise.race([Promise.all(cleanupPromises), timeoutPromise])
log.info('进程清理完成')
} catch (e) {
log.error('进程清理时出错:', e)
}
log.info('应用强制退出')
// 使用 process.exit 而不是 app.exit更加强制
setTimeout(() => {
process.exit(0)
}, 500)
}
})
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') {
isQuitting = true
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) createWindow()
})