黄中银 1 miesiąc temu
rodzic
commit
49076518f4

+ 1 - 4
ApqInstaller/electron/main.ts

@@ -91,10 +91,7 @@ function createWindow(): void {
  */
 async function initialize(): Promise<void> {
   // 初始化日志模块
-  logger.init({
-    level: isDev ? 'DEBUG' : 'INFO',
-    enableFile: true
-  })
+  logger.init()
 
   logger.info('应用启动', {
     platform: os.platform(),

+ 34 - 12
ApqInstaller/electron/modules/ipc-handlers.ts

@@ -195,6 +195,28 @@ export function registerHandlers(): void {
     return logger.getRecentLogs(200)
   })
 
+  // 写入安装日志(供渲染进程调用)
+  registerHandler('write-install-log', (_event, message: string, level: 'info' | 'warn' | 'error' = 'info') => {
+    switch (level) {
+      case 'warn':
+        logger.installWarn(message)
+        break
+      case 'error':
+        logger.installError(message)
+        break
+      default:
+        logger.installInfo(message)
+    }
+  })
+
+  // 获取日志文件路径
+  registerHandler('get-log-paths', () => {
+    return {
+      appLog: logger.getAppLogPath(),
+      installLog: logger.getInstallLogPath()
+    }
+  })
+
   // 设置窗口标题
   registerHandler('set-window-title', (_event, title: string) => {
     const windows = BrowserWindow.getAllWindows()
@@ -351,7 +373,7 @@ export function registerHandlers(): void {
   // 安装 Claude Code (通过 pnpm 或 npm 全局安装)
   registerHandler('install-claude-code', async () => {
     try {
-      logger.info('开始安装 Claude Code...')
+      logger.installInfo('开始安装 Claude Code...')
       sendToRenderer('install-status', {
         software: 'claudeCode',
         message: '正在安装 Claude Code...',
@@ -367,7 +389,7 @@ export function registerHandlers(): void {
 
       const installArgs = ['install', '-g', '@anthropic-ai/claude-code']
       const fullCommand = `${pkgManagerPath} ${installArgs.join(' ')}`
-      logger.info(`使用 ${pkgManagerName} 安装,执行命令: ${fullCommand}`)
+      logger.installInfo(`使用 ${pkgManagerName} 安装,执行命令: ${fullCommand}`)
 
       // 发送执行命令的日志
       sendToRenderer('install-status', {
@@ -388,7 +410,7 @@ export function registerHandlers(): void {
 
       // 记录安装输出
       if (result.stdout) {
-        logger.info(`${pkgManagerName} install stdout: ${result.stdout}`)
+        logger.installInfo(`${pkgManagerName} install stdout: ${result.stdout}`)
         sendToRenderer('install-status', {
           software: 'claudeCode',
           message: `${pkgManagerName} 输出: ${result.stdout}`,
@@ -398,7 +420,7 @@ export function registerHandlers(): void {
         })
       }
       if (result.stderr) {
-        logger.warn(`${pkgManagerName} install stderr: ${result.stderr}`)
+        logger.installWarn(`${pkgManagerName} install stderr: ${result.stderr}`)
         sendToRenderer('install-status', {
           software: 'claudeCode',
           message: `${pkgManagerName} 警告: ${result.stderr}`,
@@ -421,7 +443,7 @@ export function registerHandlers(): void {
 
       if (claudeExists) {
         const version = await getCommandVersionWithRefresh('claude', ['--version'])
-        logger.info(`Claude Code 安装成功: ${version}`)
+        logger.installInfo(`Claude Code 安装成功: ${version}`)
         sendToRenderer('install-complete', {
           software: 'claudeCode',
           message: '✅ Claude Code 安装完成!',
@@ -430,7 +452,7 @@ export function registerHandlers(): void {
         return { success: true }
       } else {
         // 即使验证失败,安装可能已成功,只是 PATH 还没生效
-        logger.info('Claude Code 安装完成,但验证失败(可能需要重启终端)')
+        logger.installInfo('Claude Code 安装完成,但验证失败(可能需要重启终端)')
         sendToRenderer('install-complete', {
           software: 'claudeCode',
           message: '✅ Claude Code 安装完成!(可能需要重启终端才能使用)',
@@ -452,7 +474,7 @@ export function registerHandlers(): void {
         }
       }
 
-      logger.error('Claude Code 安装失败', error)
+      logger.installError('Claude Code 安装失败', error)
       sendToRenderer('install-error', {
         software: 'claudeCode',
         message: `❌ 安装失败:${errorMessage}`,
@@ -483,13 +505,13 @@ export function registerHandlers(): void {
   // 安装 VS Code 插件
   registerHandler('install-vscode-extension', async (_event, extensionId: string) => {
     try {
-      logger.info(`开始安装 VS Code 插件: ${extensionId}`)
+      logger.installInfo(`开始安装 VS Code 插件: ${extensionId}`)
       await execa('code', ['--install-extension', extensionId], {
         encoding: 'utf8',
         stdout: 'pipe',
         stderr: 'pipe'
       })
-      logger.info(`VS Code 插件安装成功: ${extensionId}`)
+      logger.installInfo(`VS Code 插件安装成功: ${extensionId}`)
       return { success: true }
     } catch (error) {
       const execaError = error as { message?: string; stderr?: string; shortMessage?: string }
@@ -503,7 +525,7 @@ export function registerHandlers(): void {
       }
       // 从错误消息中也过滤乱码
       errorMessage = errorMessage.replace(/[^\x20-\x7E\u4e00-\u9fa5\n\r:.\-_/\\]/g, '').trim()
-      logger.error(`VS Code 插件安装失败: ${extensionId}`, error)
+      logger.installError(`VS Code 插件安装失败: ${extensionId}`, error)
       return { success: false, error: errorMessage }
     }
   })
@@ -571,11 +593,11 @@ export function registerHandlers(): void {
     // 状态回调
     const onStatus = (sw: SoftwareTypeWithAll, message: string, progress: number): void => {
       sendToRenderer('install-status', { software: sw, message, progress })
-      logger.info(`[${sw}] ${message} (${progress}%)`)
+      logger.installInfo(`[${sw}] ${message} (${progress}%)`)
     }
 
     try {
-      logger.info(`开始安装: ${software}`, options)
+      logger.installInfo(`开始安装: ${software}`, options)
 
       // Linux 下先 apt update
       if (os.platform() === 'linux' && ['nodejs', 'git', 'all'].includes(software)) {

+ 150 - 103
ApqInstaller/electron/modules/logger.ts

@@ -1,105 +1,110 @@
-// electron/modules/logger.ts - 日志模块
+// electron/modules/logger.ts - 日志模块 (基于 electron-log)
 
-import * as fs from 'fs/promises'
-import * as fsSync from 'fs'
+import log from 'electron-log/main'
 import * as path from 'path'
 import { app } from 'electron'
-import type { LogEntry } from './types'
+import type { LogEntry, LogCategory } from './types'
 
-type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'
-
-interface LoggerOptions {
-  level?: LogLevel
-  enableFile?: boolean
-  maxFileSize?: number
-  maxFiles?: number
-}
-
-const LOG_LEVELS: Record<LogLevel, number> = {
-  DEBUG: 0,
-  INFO: 1,
-  WARN: 2,
-  ERROR: 3
-}
-
-// 日志文件大小常量
+// 日志配置常量
 const MAX_LOG_FILE_SIZE = 5 * 1024 * 1024 // 5MB
 const DEFAULT_MAX_FILES = 3
 const DEFAULT_MAX_RECENT_LOGS = 500
 
+// 创建两个独立的日志实例
+const appLog = log.create({ logId: 'app' })
+const installLog = log.create({ logId: 'install' })
+
 class Logger {
-  private level: LogLevel = 'INFO'
-  private enableFile = true
-  private maxFileSize = MAX_LOG_FILE_SIZE
-  private maxFiles = DEFAULT_MAX_FILES
-  private logDir: string = ''
-  private currentLogFile: string = ''
   private recentLogs: LogEntry[] = []
   private maxRecentLogs = DEFAULT_MAX_RECENT_LOGS
+  private logDir: string = ''
+  private initialized = false
 
-  init(options: LoggerOptions = {}): void {
-    this.level = options.level || 'INFO'
-    this.enableFile = options.enableFile ?? true
-    this.maxFileSize = options.maxFileSize || this.maxFileSize
-    this.maxFiles = options.maxFiles || this.maxFiles
+  init(): void {
+    if (this.initialized) return
+    this.initialized = true
 
-    if (this.enableFile) {
-      this.logDir = path.join(app.getPath('userData'), 'logs')
-      if (!fsSync.existsSync(this.logDir)) {
-        fsSync.mkdirSync(this.logDir, { recursive: true })
-      }
-      this.currentLogFile = path.join(this.logDir, 'app.log')
-      this.rotateLogsIfNeeded()
-    }
+    this.logDir = path.join(app.getPath('userData'), 'logs')
+
+    // 配置应用日志 (app.log)
+    this.configureLogger(appLog, 'app.log')
+
+    // 配置安装日志 (install.log)
+    this.configureLogger(installLog, 'install.log')
+
+    this.info('日志系统初始化完成')
   }
 
-  private rotateLogsIfNeeded(): void {
-    if (!fsSync.existsSync(this.currentLogFile)) return
-
-    const stats = fsSync.statSync(this.currentLogFile)
-    if (stats.size >= this.maxFileSize) {
-      // 删除最旧的日志
-      for (let i = this.maxFiles - 1; i >= 1; i--) {
-        const oldFile = path.join(this.logDir, `app.${i}.log`)
-        const newFile = path.join(this.logDir, `app.${i + 1}.log`)
-        if (fsSync.existsSync(oldFile)) {
-          if (i === this.maxFiles - 1) {
-            fsSync.unlinkSync(oldFile)
-          } else {
-            fsSync.renameSync(oldFile, newFile)
+  private configureLogger(logger: typeof log, fileName: string): void {
+    // 设置日志文件路径
+    logger.transports.file.resolvePathFn = () => path.join(this.logDir, fileName)
+
+    // 文件日志配置
+    logger.transports.file.level = 'debug'
+    logger.transports.file.maxSize = MAX_LOG_FILE_SIZE
+    logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
+
+    // 控制台日志配置
+    logger.transports.console.level = 'debug'
+    logger.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}] [{level}] {text}'
+
+    // 日志轮转:保留最近的几个文件
+    logger.transports.file.archiveLogFn = (oldLogFile) => {
+      const info = path.parse(oldLogFile.path)
+      // 删除旧的轮转文件
+      for (let i = DEFAULT_MAX_FILES; i >= 1; i--) {
+        const oldFile = path.join(info.dir, `${info.name}.${i}${info.ext}`)
+        const newFile = path.join(info.dir, `${info.name}.${i + 1}${info.ext}`)
+        try {
+          const fs = require('fs')
+          if (fs.existsSync(oldFile)) {
+            if (i === DEFAULT_MAX_FILES) {
+              fs.unlinkSync(oldFile)
+            } else {
+              fs.renameSync(oldFile, newFile)
+            }
           }
+        } catch {
+          // 忽略轮转错误
         }
       }
-      fsSync.renameSync(this.currentLogFile, path.join(this.logDir, 'app.1.log'))
+      // 重命名当前日志文件
+      try {
+        const fs = require('fs')
+        fs.renameSync(oldLogFile.path, path.join(info.dir, `${info.name}.1${info.ext}`))
+      } catch {
+        // 忽略重命名错误
+      }
     }
   }
 
-  private shouldLog(level: LogLevel): boolean {
-    return LOG_LEVELS[level] >= LOG_LEVELS[this.level]
+  /**
+   * 获取日志目录路径
+   */
+  getLogDir(): string {
+    return this.logDir
   }
 
-  private formatMessage(level: LogLevel, message: string, data?: unknown): string {
-    const timestamp = new Date().toISOString()
-    let formatted = `[${timestamp}] [${level}] ${message}`
-    if (data !== undefined) {
-      formatted += ` ${JSON.stringify(data)}`
-    }
-    return formatted
+  /**
+   * 获取应用日志文件路径
+   */
+  getAppLogPath(): string {
+    return path.join(this.logDir, 'app.log')
   }
 
-  private writeToFile(formatted: string): void {
-    if (!this.enableFile || !this.currentLogFile) return
-
-    fs.appendFile(this.currentLogFile, formatted + '\n')
-      .then(() => this.rotateLogsIfNeeded())
-      .catch((error) => console.error('写入日志文件失败:', error))
+  /**
+   * 获取安装日志文件路径
+   */
+  getInstallLogPath(): string {
+    return path.join(this.logDir, 'install.log')
   }
 
-  private addToRecent(level: LogLevel, message: string, data?: unknown): void {
+  private addToRecent(level: LogEntry['level'], message: string, category: LogCategory, data?: unknown): void {
     const entry: LogEntry = {
       level,
       message,
       timestamp: new Date().toISOString(),
+      category,
       data
     }
     this.recentLogs.push(entry)
@@ -108,51 +113,93 @@ class Logger {
     }
   }
 
-  private log(level: LogLevel, message: string, data?: unknown): void {
-    if (!this.shouldLog(level)) return
-
-    const formatted = this.formatMessage(level, message, data)
-
-    // 控制台输出
-    switch (level) {
-      case 'DEBUG':
-        console.debug(formatted)
-        break
-      case 'INFO':
-        console.info(formatted)
-        break
-      case 'WARN':
-        console.warn(formatted)
-        break
-      case 'ERROR':
-        console.error(formatted)
-        break
-    }
-
-    // 文件输出
-    this.writeToFile(formatted)
-
-    // 保存到最近日志
-    this.addToRecent(level, message, data)
-  }
+  // ==================== 应用日志方法 ====================
 
   debug(message: string, data?: unknown): void {
-    this.log('DEBUG', message, data)
+    if (data !== undefined) {
+      appLog.debug(message, data)
+    } else {
+      appLog.debug(message)
+    }
+    this.addToRecent('DEBUG', message, 'app', data)
   }
 
   info(message: string, data?: unknown): void {
-    this.log('INFO', message, data)
+    if (data !== undefined) {
+      appLog.info(message, data)
+    } else {
+      appLog.info(message)
+    }
+    this.addToRecent('INFO', message, 'app', data)
   }
 
   warn(message: string, data?: unknown): void {
-    this.log('WARN', message, data)
+    if (data !== undefined) {
+      appLog.warn(message, data)
+    } else {
+      appLog.warn(message)
+    }
+    this.addToRecent('WARN', message, 'app', data)
   }
 
   error(message: string, data?: unknown): void {
-    this.log('ERROR', message, data)
+    if (data !== undefined) {
+      appLog.error(message, data)
+    } else {
+      appLog.error(message)
+    }
+    this.addToRecent('ERROR', message, 'app', data)
+  }
+
+  // ==================== 安装日志方法 ====================
+
+  installDebug(message: string, data?: unknown): void {
+    if (data !== undefined) {
+      installLog.debug(message, data)
+    } else {
+      installLog.debug(message)
+    }
+    this.addToRecent('DEBUG', message, 'install', data)
+  }
+
+  installInfo(message: string, data?: unknown): void {
+    if (data !== undefined) {
+      installLog.info(message, data)
+    } else {
+      installLog.info(message)
+    }
+    this.addToRecent('INFO', message, 'install', data)
+  }
+
+  installWarn(message: string, data?: unknown): void {
+    if (data !== undefined) {
+      installLog.warn(message, data)
+    } else {
+      installLog.warn(message)
+    }
+    this.addToRecent('WARN', message, 'install', data)
   }
 
-  getRecentLogs(limit = 200): LogEntry[] {
+  installError(message: string, data?: unknown): void {
+    if (data !== undefined) {
+      installLog.error(message, data)
+    } else {
+      installLog.error(message)
+    }
+    this.addToRecent('ERROR', message, 'install', data)
+  }
+
+  // ==================== 日志查询方法 ====================
+
+  /**
+   * 获取最近的日志
+   * @param limit 返回的日志条数
+   * @param category 可选,筛选特定类别的日志
+   */
+  getRecentLogs(limit = 200, category?: LogCategory): LogEntry[] {
+    if (category) {
+      return this.recentLogs.filter(log => log.category === category).slice(-limit)
+    }
     return this.recentLogs.slice(-limit)
   }
 

+ 6 - 0
ApqInstaller/electron/preload.ts

@@ -68,6 +68,12 @@ const electronAPI: ElectronAPI = {
   // 获取日志
   getLogs: () => ipcRenderer.invoke('get-logs'),
 
+  // 写入安装日志
+  writeInstallLog: (message: string, level?: 'info' | 'warn' | 'error') => ipcRenderer.invoke('write-install-log', message, level),
+
+  // 获取日志文件路径
+  getLogPaths: () => ipcRenderer.invoke('get-log-paths'),
+
   // ==================== 窗口操作 ====================
 
   // 设置窗口标题

+ 10 - 0
ApqInstaller/package-lock.json

@@ -9,6 +9,7 @@
       "version": "0.0.5",
       "dependencies": {
         "@element-plus/icons-vue": "^2.3.1",
+        "electron-log": "^5.4.3",
         "electron-updater": "^6.6.2",
         "element-plus": "^2.9.0",
         "execa": "^9.6.1",
@@ -5099,6 +5100,15 @@
         "node": ">= 10.0.0"
       }
     },
+    "node_modules/electron-log": {
+      "version": "5.4.3",
+      "resolved": "https://registry.npmmirror.com/electron-log/-/electron-log-5.4.3.tgz",
+      "integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/electron-publish": {
       "version": "26.0.11",
       "resolved": "https://registry.npmmirror.com/electron-publish/-/electron-publish-26.0.11.tgz",

+ 2 - 1
ApqInstaller/package.json

@@ -25,6 +25,7 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
+    "electron-log": "^5.4.3",
     "electron-updater": "^6.6.2",
     "element-plus": "^2.9.0",
     "execa": "^9.6.1",
@@ -40,6 +41,7 @@
     "@typescript-eslint/eslint-plugin": "^8.48.1",
     "@typescript-eslint/parser": "^8.48.1",
     "@vitejs/plugin-vue": "^6.0.2",
+    "@vitest/coverage-v8": "^3.2.4",
     "@vue/eslint-config-prettier": "^10.2.0",
     "@vue/eslint-config-typescript": "^14.6.0",
     "electron": "^39.2.5",
@@ -54,7 +56,6 @@
     "vite-plugin-electron": "^0.29.0",
     "vite-plugin-electron-renderer": "^0.14.6",
     "vitest": "^3.2.4",
-    "@vitest/coverage-v8": "^3.2.4",
     "vue-tsc": "^3.1.5"
   },
   "build": {

+ 5 - 0
ApqInstaller/shared/types.ts

@@ -96,10 +96,13 @@ export interface CommandResult {
   args: string[]
 }
 
+export type LogCategory = 'app' | 'install'
+
 export interface LogEntry {
   level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'
   message: string
   timestamp: string
+  category?: LogCategory
   data?: unknown
 }
 
@@ -175,6 +178,8 @@ export interface ElectronAPI {
   // 历史和日志
   getInstallHistory: (limit?: number) => Promise<InstallHistoryItem[]>
   getLogs: () => Promise<LogEntry[]>
+  writeInstallLog: (message: string, level?: 'info' | 'warn' | 'error') => Promise<void>
+  getLogPaths: () => Promise<{ appLog: string; installLog: string }>
 
   // 窗口操作
   setWindowTitle: (title: string) => Promise<void>

+ 1 - 1
ApqInstaller/src/App.vue

@@ -94,7 +94,7 @@ function bindInstallListeners() {
   window.electronAPI.onInstallError((data) => {
     const translatedMessage = getTranslatedMessage(data.message, data.i18nKey, data.i18nParams)
     installStore.setError(data.software, translatedMessage)
-    installStore.addLog(translatedMessage)
+    installStore.addLog(translatedMessage, 'error')
     // 安装失败后也重新检测所有软件的安装状态
     installStore.checkInstalledSoftware()
   })

+ 5 - 1
ApqInstaller/src/stores/install.ts

@@ -189,13 +189,17 @@ export const useInstallStore = defineStore('install', () => {
     }
   }
 
-  function addLog(message: string): void {
+  function addLog(message: string, level: 'info' | 'warn' | 'error' = 'info'): void {
     const timestamp = new Date().toLocaleTimeString()
     installLogs.value.push(`[${timestamp}] ${message}`)
     // 限制日志数量
     if (installLogs.value.length > 500) {
       installLogs.value = installLogs.value.slice(-500)
     }
+    // 同时写入日志文件
+    window.electronAPI.writeInstallLog(message, level).catch(() => {
+      // 忽略写入失败
+    })
   }
 
   function clearLogs(): void {