From 6b00c8a6be814abd4882cc97fb1eb4102c73146a Mon Sep 17 00:00:00 2001 From: AoXuan Date: Tue, 2 Sep 2025 18:20:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=B7=BB=E5=8A=A0=E5=9B=9Elog?= =?UTF-8?q?=E5=8A=9F=E8=83=BD~=E8=BF=99=E6=AC=A1=E5=A5=BD=E7=94=A8?= =?UTF-8?q?=E4=BA=86~?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/electron/main.ts | 91 +++++++++- frontend/electron/preload.ts | 8 +- frontend/electron/services/logService.ts | 140 +++++++++++++++ frontend/electron/services/pythonService.ts | 34 ++-- frontend/package.json | 1 + frontend/src/App.vue | 3 + frontend/src/components/LogViewer.vue | 189 ++++++++++++++++++++ frontend/src/main.ts | 14 ++ frontend/src/types/electron.d.ts | 42 +++-- frontend/src/utils/logger.ts | 89 +++++++++ frontend/yarn.lock | 8 + 11 files changed, 582 insertions(+), 37 deletions(-) create mode 100644 frontend/electron/services/logService.ts create mode 100644 frontend/src/components/LogViewer.vue create mode 100644 frontend/src/utils/logger.ts diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index a346956..78ebef3 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -13,6 +13,7 @@ import { stopBackend, } from './services/pythonService' import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService' +import { setupLogger, log, getLogPath, cleanOldLogs } from './services/logService' // 检查是否以管理员权限运行 function isRunningAsAdmin(): boolean { @@ -60,6 +61,8 @@ function restartAsAdmin(): void { let mainWindow: BrowserWindow | null = null function createWindow() { + log.info('开始创建主窗口') + mainWindow = new BrowserWindow({ width: 1600, height: 1000, @@ -77,13 +80,16 @@ function createWindow() { 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 }) @@ -92,6 +98,7 @@ function createWindow() { setDownloadMainWindow(mainWindow) setPythonMainWindow(mainWindow) setGitMainWindow(mainWindow) + log.info('主窗口创建完成,服务引用已设置') } } @@ -240,6 +247,62 @@ ipcMain.handle('reset-config', async () => { }) // 日志文件操作 +ipcMain.handle('get-log-path', async () => { + try { + return getLogPath() + } catch (error) { + log.error('获取日志路径失败:', error) + throw error + } +}) + +ipcMain.handle('get-logs', async (event, lines?: number) => { + try { + const 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 () => { + try { + const logFilePath = getLogPath() + + if (fs.existsSync(logFilePath)) { + fs.writeFileSync(logFilePath, '', 'utf8') + log.info('日志文件已清空') + } + } 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() @@ -252,9 +315,9 @@ ipcMain.handle('save-logs-to-file', async (event, logs: string) => { const logFilePath = path.join(logsDir, 'app.log') fs.writeFileSync(logFilePath, logs, 'utf8') - console.log(`日志已保存到: ${logFilePath}`) + log.info(`日志已保存到: ${logFilePath}`) } catch (error) { - console.error('保存日志文件失败:', error) + log.error('保存日志文件失败:', error) throw error } }) @@ -266,13 +329,13 @@ ipcMain.handle('load-logs-from-file', async () => { if (fs.existsSync(logFilePath)) { const logs = fs.readFileSync(logFilePath, 'utf8') - console.log(`从文件加载日志: ${logFilePath}`) + log.info(`从文件加载日志: ${logFilePath}`) return logs } return null } catch (error) { - console.error('加载日志文件失败:', error) + log.error('加载日志文件失败:', error) return null } }) @@ -305,22 +368,42 @@ app.on('second-instance', () => { 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() }) diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index 4db6e31..2134795 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -1,7 +1,7 @@ import { contextBridge, ipcRenderer } from 'electron' window.addEventListener('DOMContentLoaded', () => { - console.log('Preload loaded') + console.log('预加载脚本已加载') }) // 暴露安全的 API 给渲染进程 @@ -31,6 +31,12 @@ contextBridge.exposeInMainWorld('electronAPI', { resetConfig: () => ipcRenderer.invoke('reset-config'), // 日志文件操作 + getLogPath: () => ipcRenderer.invoke('get-log-path'), + getLogs: (lines?: number) => ipcRenderer.invoke('get-logs', lines), + clearLogs: () => ipcRenderer.invoke('clear-logs'), + cleanOldLogs: (daysToKeep?: number) => ipcRenderer.invoke('clean-old-logs', daysToKeep), + + // 保留原有方法以兼容现有代码 saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs), loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'), diff --git a/frontend/electron/services/logService.ts b/frontend/electron/services/logService.ts new file mode 100644 index 0000000..c2196b3 --- /dev/null +++ b/frontend/electron/services/logService.ts @@ -0,0 +1,140 @@ +import log from 'electron-log' +import * as path from 'path' +import { getAppRoot } from './environmentService' + +// 移除ANSI颜色转义字符的函数 +function stripAnsiColors(text: string): string { + // 匹配ANSI转义序列的正则表达式 - 更完整的模式 + const ansiRegex = /\x1b\[[0-9;]*[mGKHF]|\x1b\[[\d;]*[A-Za-z]/g + return text.replace(ansiRegex, '') +} + +// 获取应用安装目录下的日志路径 +function getLogDirectory(): string { + const appRoot = getAppRoot() + return path.join(appRoot, 'logs') +} + +// 配置日志系统 +export function setupLogger() { + // 设置日志文件路径到软件安装目录 + const logPath = getLogDirectory() + + // 确保日志目录存在 + const fs = require('fs') + if (!fs.existsSync(logPath)) { + fs.mkdirSync(logPath, { recursive: true }) + } + + // 配置日志格式 + log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}' + log.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}' + + // 设置主进程日志文件路径和名称 + log.transports.file.resolvePathFn = () => path.join(logPath, 'app.log') + + // 设置日志级别 + log.transports.file.level = 'debug' + log.transports.console.level = 'debug' + + // 设置文件大小限制 (10MB) + log.transports.file.maxSize = 10 * 1024 * 1024 + + // 保留最近的5个日志文件 + log.transports.file.archiveLog = (file: any) => { + const filePath = file.toString() + const info = path.parse(filePath) + + try { + return path.join(info.dir, `${info.name}.old${info.ext}`) + } catch (e) { + console.warn('Could not archive log file', e) + return null + } + } + + // 捕获未处理的异常和Promise拒绝 + log.catchErrors({ + showDialog: false, + onError: (options: any) => { + log.error('未处理的错误:', options.error) + log.error('版本信息:', options.versions) + log.error('进程类型:', options.processType) + }, + }) + + // 重写console方法,将所有控制台输出重定向到日志 + const originalConsole = { + log: console.log, + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug, + } + + console.log = (...args) => { + log.info(...args) + originalConsole.log(...args) + } + + console.error = (...args) => { + log.error(...args) + originalConsole.error(...args) + } + + console.warn = (...args) => { + log.warn(...args) + originalConsole.warn(...args) + } + + console.info = (...args) => { + log.info(...args) + originalConsole.info(...args) + } + + console.debug = (...args) => { + log.debug(...args) + originalConsole.debug(...args) + } + + log.info('日志系统初始化完成') + log.info(`日志文件路径: ${path.join(logPath, 'main.log')}`) + + return log +} + +// 导出日志实例和工具函数 +export { log, stripAnsiColors } + +// 获取日志文件路径 +export function getLogPath(): string { + return path.join(getLogDirectory(), 'main.log') +} + +// 清理旧日志文件 +export function cleanOldLogs(daysToKeep: number = 7) { + const fs = require('fs') + const logDir = getLogDirectory() + + if (!fs.existsSync(logDir)) { + return + } + + const files = fs.readdirSync(logDir) + const now = Date.now() + const maxAge = daysToKeep * 24 * 60 * 60 * 1000 // 转换为毫秒 + + files.forEach((file: string) => { + const filePath = path.join(logDir, file) + const stats = fs.statSync(filePath) + + if (now - stats.mtime.getTime() > maxAge) { + try { + fs.unlinkSync(filePath) + log.info(`已删除旧日志文件: ${file}`) + } catch (error) { + log.error(`删除旧日志文件失败: ${file}`, error) + } + } + }) +} \ No newline at end of file diff --git a/frontend/electron/services/pythonService.ts b/frontend/electron/services/pythonService.ts index b533230..24da491 100644 --- a/frontend/electron/services/pythonService.ts +++ b/frontend/electron/services/pythonService.ts @@ -5,6 +5,8 @@ import { BrowserWindow } from 'electron' import AdmZip from 'adm-zip' import { downloadFile } from './downloadService' import { ChildProcessWithoutNullStreams } from 'node:child_process' +import { log, stripAnsiColors } from './logService' + let mainWindow: BrowserWindow | null = null @@ -101,13 +103,13 @@ async function installPip(pythonPath: string, appRoot: string): Promise { }) process.stdout?.on('data', data => { - const output = data.toString() - console.log('pip安装输出:', output) + const output = stripAnsiColors(data.toString()) + log.info('pip安装输出:', output) }) process.stderr?.on('data', data => { - const errorOutput = data.toString() - console.log('pip安装错误输出:', errorOutput) + const errorOutput = stripAnsiColors(data.toString()) + log.warn('pip安装错误输出:', errorOutput) }) process.on('close', code => { @@ -135,13 +137,13 @@ async function installPip(pythonPath: string, appRoot: string): Promise { }) verifyProcess.stdout?.on('data', data => { - const output = data.toString() - console.log('pip版本信息:', output) + const output = stripAnsiColors(data.toString()) + log.info('pip版本信息:', output) }) verifyProcess.stderr?.on('data', data => { - const errorOutput = data.toString() - console.log('pip版本检查错误:', errorOutput) + const errorOutput = stripAnsiColors(data.toString()) + log.warn('pip版本检查错误:', errorOutput) }) verifyProcess.on('close', code => { @@ -367,8 +369,8 @@ export async function installDependencies( ) process.stdout?.on('data', data => { - const output = data.toString() - console.log('Pip output:', output) + const output = stripAnsiColors(data.toString()) + log.info('Pip output:', output) if (mainWindow) { mainWindow.webContents.send('download-progress', { @@ -381,8 +383,8 @@ export async function installDependencies( }) process.stderr?.on('data', data => { - const errorOutput = data.toString() - console.error('Pip error:', errorOutput) + const errorOutput = stripAnsiColors(data.toString()) + log.error('Pip error:', errorOutput) }) process.on('close', code => { @@ -508,12 +510,12 @@ export async function startBackend(appRoot: string, timeoutMs = 30_000) { backendProc.stderr.setEncoding('utf8') backendProc.stdout.on('data', d => { - const line = d.toString().trim() - if (line) console.log('[Backend]', line) + const line = stripAnsiColors(d.toString().trim()) + if (line) log.info('[Backend]', line) }) backendProc.stderr.on('data', d => { - const line = d.toString().trim() - if (line) console.log('[Backend]', line) + const line = stripAnsiColors(d.toString().trim()) + if (line) log.info('[Backend]', line) }) backendProc.once('exit', (code, signal) => { diff --git a/frontend/package.json b/frontend/package.json index 6d98394..f23d804 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -53,6 +53,7 @@ "ant-design-vue": "4.x", "axios": "^1.11.0", "dayjs": "^1.11.13", + "electron-log": "^5.4.3", "form-data": "^4.0.4", "markdown-it": "^14.1.0", "vue": "^3.5.17", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c11ee23..36e57aa 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -5,6 +5,7 @@ import { ConfigProvider } from 'ant-design-vue' import { useTheme } from './composables/useTheme.ts' import AppLayout from './components/AppLayout.vue' import zhCN from 'ant-design-vue/es/locale/zh_CN' +import { logger } from '@/utils/logger' const route = useRoute() const { antdTheme, initTheme } = useTheme() @@ -13,7 +14,9 @@ const { antdTheme, initTheme } = useTheme() const isInitializationPage = computed(() => route.name === 'Initialization') onMounted(() => { + logger.info('App组件已挂载') initTheme() + logger.info('主题初始化完成') }) diff --git a/frontend/src/components/LogViewer.vue b/frontend/src/components/LogViewer.vue new file mode 100644 index 0000000..94cdc05 --- /dev/null +++ b/frontend/src/components/LogViewer.vue @@ -0,0 +1,189 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 97e2a2d..78adbce 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -9,6 +9,9 @@ import zhCN from 'ant-design-vue/es/locale/zh_CN' import dayjs from 'dayjs' import 'dayjs/locale/zh-cn' +// 导入日志系统 +import { logger } from '@/utils/logger' + // 配置dayjs中文本地化 dayjs.locale('zh-cn') @@ -17,6 +20,10 @@ import { API_ENDPOINTS } from '@/config/mirrors' // 配置API基础URL OpenAPI.BASE = API_ENDPOINTS.local +// 记录应用启动 +logger.info('前端应用开始初始化') +logger.info(`API基础URL: ${OpenAPI.BASE}`) + // 创建应用实例 const app = createApp(App) @@ -24,5 +31,12 @@ const app = createApp(App) app.use(Antd) app.use(router) +// 全局错误处理 +app.config.errorHandler = (err, instance, info) => { + logger.error('Vue应用错误:', err, '组件信息:', info) +} + // 挂载应用 app.mount('#app') + +logger.info('前端应用初始化完成') diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index caa2227..1a8ef81 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -1,28 +1,38 @@ export interface ElectronAPI { openDevTools: () => Promise selectFolder: () => Promise - selectFile: (filters?: any[]) => Promise + selectFile: (filters?: any[]) => Promise openUrl: (url: string) => Promise<{ success: boolean; error?: string }> // 初始化相关API - checkEnvironment: () => Promise<{ - pythonExists: boolean - gitExists: boolean - backendExists: boolean - dependenciesInstalled: boolean - isInitialized: boolean - }> - downloadPython: (mirror?: string) => Promise<{ success: boolean; error?: string }> - downloadGit: () => Promise<{ success: boolean; error?: string }> - installDependencies: (mirror?: string) => Promise<{ success: boolean; error?: string }> - cloneBackend: (repoUrl?: string) => Promise<{ success: boolean; error?: string }> - updateBackend: (repoUrl?: string) => Promise<{ success: boolean; error?: string }> - startBackend: () => Promise<{ success: boolean; error?: string }> - + checkEnvironment: () => Promise + downloadPython: (mirror?: string) => Promise + installPip: () => Promise + downloadGit: () => Promise + installDependencies: (mirror?: string) => Promise + cloneBackend: (repoUrl?: string) => Promise + updateBackend: (repoUrl?: string) => Promise + startBackend: () => Promise + + // 管理员权限相关 + checkAdmin: () => Promise + restartAsAdmin: () => Promise + + // 配置文件操作 + saveConfig: (config: any) => Promise + loadConfig: () => Promise + resetConfig: () => Promise + // 日志文件操作 + getLogPath: () => Promise + getLogs: (lines?: number) => Promise + clearLogs: () => Promise + cleanOldLogs: (daysToKeep?: number) => Promise + + // 保留原有方法以兼容现有代码 saveLogsToFile: (logs: string) => Promise loadLogsFromFile: () => Promise - + // 监听下载进度 onDownloadProgress: (callback: (progress: any) => void) => void removeDownloadProgressListener: () => void diff --git a/frontend/src/utils/logger.ts b/frontend/src/utils/logger.ts new file mode 100644 index 0000000..200e4e3 --- /dev/null +++ b/frontend/src/utils/logger.ts @@ -0,0 +1,89 @@ +// 渲染进程日志工具 +interface ElectronAPI { + getLogPath: () => Promise + getLogs: (lines?: number) => Promise + clearLogs: () => Promise + cleanOldLogs: (daysToKeep?: number) => Promise +} + +declare global { + interface Window { + electronAPI: ElectronAPI + } +} + +export enum LogLevel { + DEBUG = 'DEBUG', + INFO = 'INFO', + WARN = 'WARN', + ERROR = 'ERROR' +} + +class Logger { + // 直接使用原生console,主进程会自动处理日志记录 + debug(message: string, ...args: any[]) { + console.debug(message, ...args) + } + + info(message: string, ...args: any[]) { + console.info(message, ...args) + } + + warn(message: string, ...args: any[]) { + console.warn(message, ...args) + } + + error(message: string, ...args: any[]) { + console.error(message, ...args) + } + + // 获取日志文件路径 + async getLogPath(): Promise { + if (window.electronAPI) { + return await window.electronAPI.getLogPath() + } + throw new Error('Electron API not available') + } + + // 获取日志内容 + async getLogs(lines?: number): Promise { + if (window.electronAPI) { + return await window.electronAPI.getLogs(lines) + } + throw new Error('Electron API not available') + } + + // 清空日志 + async clearLogs(): Promise { + if (window.electronAPI) { + await window.electronAPI.clearLogs() + console.info('日志已清空') + } else { + throw new Error('Electron API not available') + } + } + + // 清理旧日志 + async cleanOldLogs(daysToKeep: number = 7): Promise { + if (window.electronAPI) { + await window.electronAPI.cleanOldLogs(daysToKeep) + console.info(`已清理${daysToKeep}天前的旧日志`) + } else { + throw new Error('Electron API not available') + } + } +} + +// 创建全局日志实例 +export const logger = new Logger() + +// 捕获未处理的错误(直接使用console,主进程会处理日志记录) +window.addEventListener('error', (event) => { + console.error('未处理的错误:', event.error?.message || event.message, event.error?.stack) +}) + +window.addEventListener('unhandledrejection', (event) => { + console.error('未处理的Promise拒绝:', event.reason) +}) + +export default logger \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 7064991..f3a47cf 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2449,6 +2449,13 @@ __metadata: languageName: node linkType: hard +"electron-log@npm:^5.4.3": + version: 5.4.3 + resolution: "electron-log@npm:5.4.3" + checksum: 10c0/84d398d2a53cd73fdd1118f7b376b08d74938c79cb4aa3454aa176b61ebf1522f5cc92179355e69e0053301df7a19fbe6cf302319e5fbe5fc6a1600f282a2646 + languageName: node + linkType: hard + "electron-publish@npm:26.0.11": version: 26.0.11 resolution: "electron-publish@npm:26.0.11" @@ -3061,6 +3068,7 @@ __metadata: dayjs: "npm:^1.11.13" electron: "npm:^37.2.5" electron-builder: "npm:^26.0.12" + electron-log: "npm:^5.4.3" eslint: "npm:^9.32.0" eslint-config-prettier: "npm:^10.1.8" eslint-plugin-prettier: "npm:^5.5.3"