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

444 lines
11 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 } 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 mainWindow: BrowserWindow | null = null
function createWindow() {
log.info('开始创建主窗口')
mainWindow = new BrowserWindow({
width: 1600,
height: 1000,
minWidth: 800,
minHeight: 600,
icon: path.join(__dirname, '../src/assets/AUTO_MAA.ico'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
},
autoHideMenuBar: true,
})
mainWindow.setMenuBarVisibility(false)
const devServer = process.env.VITE_DEV_SERVER_URL
if (devServer) {
log.info(`加载开发服务器: ${devServer}`)
mainWindow.loadURL(devServer)
} else {
const indexHtmlPath = path.join(app.getAppPath(), 'dist', 'index.html')
log.info(`加载生产环境页面: ${indexHtmlPath}`)
mainWindow.loadFile(indexHtmlPath)
}
mainWindow.on('closed', () => {
log.info('主窗口已关闭')
mainWindow = null
})
// 设置各个服务的主窗口引用
if (mainWindow) {
setDownloadMainWindow(mainWindow)
setPythonMainWindow(mainWindow)
setGitMainWindow(mainWindow)
log.info('主窗口创建完成,服务引用已设置')
}
}
// IPC处理函数
ipcMain.handle('open-dev-tools', () => {
if (mainWindow) {
mainWindow.webContents.openDevTools({ mode: 'undocked' })
}
})
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('check-environment', async () => {
const appRoot = getAppRoot()
return checkEnvironment(appRoot)
})
// 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)
})
// Git相关
ipcMain.handle('download-git', async () => {
const appRoot = getAppRoot()
return downloadGit(appRoot)
})
ipcMain.handle(
'clone-backend',
async (event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => {
const appRoot = getAppRoot()
return cloneBackend(appRoot, repoUrl)
}
)
ipcMain.handle(
'update-backend',
async (event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.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}`)
} catch (error) {
console.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 () => {
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 => {
// 只处理一次,避免多重触发
event.preventDefault()
log.info('应用准备退出')
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()
})