logger.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. // electron/modules/logger.ts - 日志模块 (基于 electron-log)
  2. import log from 'electron-log/main'
  3. import * as path from 'path'
  4. import * as fs from 'fs'
  5. import { app } from 'electron'
  6. import type { LogEntry, LogCategory } from './types'
  7. // 日志配置常量
  8. const MAX_LOG_FILE_SIZE = 5 * 1024 * 1024 // 5MB
  9. const DEFAULT_MAX_FILES = 3
  10. const DEFAULT_MAX_RECENT_LOGS = 500
  11. // 日志实例
  12. let appLog: ReturnType<typeof log.create> | null = null
  13. let installLog: ReturnType<typeof log.create> | null = null
  14. class Logger {
  15. private recentLogs: LogEntry[] = []
  16. private maxRecentLogs = DEFAULT_MAX_RECENT_LOGS
  17. private logDir: string = ''
  18. private initialized = false
  19. init(): void {
  20. if (this.initialized) return
  21. this.initialized = true
  22. // 日志目录设置 - 统一使用 Electron 推荐的用户数据目录
  23. // Windows: C:\Users\用户名\AppData\Roaming\claude-ai-installer\logs
  24. const userDataDir = app.getPath('userData')
  25. console.log('[Logger] 使用用户数据目录:', userDataDir)
  26. this.logDir = path.join(userDataDir, 'logs')
  27. // 确保日志目录存在
  28. console.log('[Logger] 尝试创建日志目录:', this.logDir)
  29. try {
  30. if (!fs.existsSync(this.logDir)) {
  31. fs.mkdirSync(this.logDir, { recursive: true })
  32. console.log('[Logger] 日志目录创建成功')
  33. } else {
  34. console.log('[Logger] 日志目录已存在')
  35. }
  36. } catch (err) {
  37. console.error('[Logger] 创建日志目录失败:', err)
  38. }
  39. // 保存日志目录路径供 resolvePathFn 使用
  40. const logDir = this.logDir
  41. // 创建应用日志实例
  42. console.log('[Logger] 创建 appLog 实例...')
  43. appLog = log.create({ logId: 'app' })
  44. const appLogPath = path.join(logDir, 'app.log')
  45. appLog.transports.file.resolvePathFn = () => appLogPath
  46. appLog.transports.file.level = 'debug'
  47. appLog.transports.file.maxSize = MAX_LOG_FILE_SIZE
  48. appLog.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
  49. appLog.transports.file.archiveLogFn = (file) => this.archiveLog(file)
  50. appLog.transports.console.level = 'debug'
  51. appLog.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}] [{level}] {text}'
  52. console.log('[Logger] appLog 配置完成,路径:', appLogPath)
  53. // 创建安装日志实例
  54. console.log('[Logger] 创建 installLog 实例...')
  55. installLog = log.create({ logId: 'install' })
  56. const installLogPath = path.join(logDir, 'install.log')
  57. installLog.transports.file.resolvePathFn = () => installLogPath
  58. installLog.transports.file.level = 'debug'
  59. installLog.transports.file.maxSize = MAX_LOG_FILE_SIZE
  60. installLog.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
  61. installLog.transports.file.archiveLogFn = (file) => this.archiveLog(file)
  62. installLog.transports.console.level = 'debug'
  63. installLog.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}] [{level}] {text}'
  64. console.log('[Logger] installLog 配置完成,路径:', installLogPath)
  65. // 写入初始化日志
  66. this.info('日志系统初始化完成', { logDir: this.logDir })
  67. this.installInfo('安装日志系统初始化完成', { logDir: this.logDir })
  68. }
  69. /**
  70. * 日志文件轮转
  71. */
  72. private archiveLog(file: { path: string; toString: () => string }): void {
  73. const filePath = file.toString()
  74. const info = path.parse(filePath)
  75. // 删除最旧的轮转文件
  76. for (let i = DEFAULT_MAX_FILES; i >= 1; i--) {
  77. const oldFile = path.join(info.dir, `${info.name}.${i}${info.ext}`)
  78. const newFile = path.join(info.dir, `${info.name}.${i + 1}${info.ext}`)
  79. try {
  80. if (fs.existsSync(oldFile)) {
  81. if (i === DEFAULT_MAX_FILES) {
  82. fs.unlinkSync(oldFile)
  83. } else {
  84. fs.renameSync(oldFile, newFile)
  85. }
  86. }
  87. } catch {
  88. // 忽略轮转错误
  89. }
  90. }
  91. // 重命名当前日志文件
  92. try {
  93. fs.renameSync(filePath, path.join(info.dir, `${info.name}.1${info.ext}`))
  94. } catch {
  95. // 忽略重命名错误
  96. }
  97. }
  98. getLogDir(): string {
  99. return this.logDir
  100. }
  101. getAppLogPath(): string {
  102. return path.join(this.logDir, 'app.log')
  103. }
  104. getInstallLogPath(): string {
  105. return path.join(this.logDir, 'install.log')
  106. }
  107. private addToRecent(level: LogEntry['level'], message: string, category: LogCategory, data?: unknown): void {
  108. const entry: LogEntry = {
  109. level,
  110. message,
  111. timestamp: new Date().toISOString(),
  112. category,
  113. data
  114. }
  115. this.recentLogs.push(entry)
  116. if (this.recentLogs.length > this.maxRecentLogs) {
  117. this.recentLogs.shift()
  118. }
  119. }
  120. // ==================== 应用日志方法 ====================
  121. debug(message: string, data?: unknown): void {
  122. if (appLog) {
  123. if (data !== undefined) {
  124. appLog.debug(message, data)
  125. } else {
  126. appLog.debug(message)
  127. }
  128. } else {
  129. console.debug('[Logger]', message, data ?? '')
  130. }
  131. this.addToRecent('DEBUG', message, 'app', data)
  132. }
  133. info(message: string, data?: unknown): void {
  134. if (appLog) {
  135. if (data !== undefined) {
  136. appLog.info(message, data)
  137. } else {
  138. appLog.info(message)
  139. }
  140. } else {
  141. console.info('[Logger]', message, data ?? '')
  142. }
  143. this.addToRecent('INFO', message, 'app', data)
  144. }
  145. warn(message: string, data?: unknown): void {
  146. if (appLog) {
  147. if (data !== undefined) {
  148. appLog.warn(message, data)
  149. } else {
  150. appLog.warn(message)
  151. }
  152. } else {
  153. console.warn('[Logger]', message, data ?? '')
  154. }
  155. this.addToRecent('WARN', message, 'app', data)
  156. }
  157. error(message: string, data?: unknown): void {
  158. if (appLog) {
  159. if (data !== undefined) {
  160. appLog.error(message, data)
  161. } else {
  162. appLog.error(message)
  163. }
  164. } else {
  165. console.error('[Logger]', message, data ?? '')
  166. }
  167. this.addToRecent('ERROR', message, 'app', data)
  168. }
  169. // ==================== 安装日志方法 ====================
  170. installDebug(message: string, data?: unknown): void {
  171. if (installLog) {
  172. if (data !== undefined) {
  173. installLog.debug(message, data)
  174. } else {
  175. installLog.debug(message)
  176. }
  177. } else {
  178. console.debug('[InstallLog]', message, data ?? '')
  179. }
  180. this.addToRecent('DEBUG', message, 'install', data)
  181. }
  182. installInfo(message: string, data?: unknown): void {
  183. if (installLog) {
  184. if (data !== undefined) {
  185. installLog.info(message, data)
  186. } else {
  187. installLog.info(message)
  188. }
  189. } else {
  190. console.info('[InstallLog]', message, data ?? '')
  191. }
  192. this.addToRecent('INFO', message, 'install', data)
  193. }
  194. installWarn(message: string, data?: unknown): void {
  195. if (installLog) {
  196. if (data !== undefined) {
  197. installLog.warn(message, data)
  198. } else {
  199. installLog.warn(message)
  200. }
  201. } else {
  202. console.warn('[InstallLog]', message, data ?? '')
  203. }
  204. this.addToRecent('WARN', message, 'install', data)
  205. }
  206. installError(message: string, data?: unknown): void {
  207. if (installLog) {
  208. if (data !== undefined) {
  209. installLog.error(message, data)
  210. } else {
  211. installLog.error(message)
  212. }
  213. } else {
  214. console.error('[InstallLog]', message, data ?? '')
  215. }
  216. this.addToRecent('ERROR', message, 'install', data)
  217. }
  218. // ==================== 日志查询方法 ====================
  219. getRecentLogs(limit = 200, category?: LogCategory): LogEntry[] {
  220. if (category) {
  221. return this.recentLogs.filter(log => log.category === category).slice(-limit)
  222. }
  223. return this.recentLogs.slice(-limit)
  224. }
  225. close(): void {
  226. this.recentLogs = []
  227. }
  228. }
  229. export const logger = new Logger()
  230. export default logger