返回 oh-my-ppt
application.ts
根目录 / src / main / app / application.ts
1 import { app, BrowserWindow } from 'electron'
2 import { electronApp, is, optimizer } from '@electron-toolkit/utils'
3 import log from 'electron-log/main.js'
4 import { join } from 'path'
5 import { AgentManager } from '../agent-runtime/agent'
6 import { PPTDatabase } from '../db/database'
7 import { configureHtmlThumbnailService } from '../io/thumbnails/html-thumbnail-service'
8 import { registerLocalAssetProtocol, setupIPC } from '../ipc'
9 import {
10 initializeSkills,
11 resolveBuiltinSkillsSourcePath,
12 resolveInstalledSkillsPath,
13 setSkillsRuntime
14 } from '../product-skills'
15 import {
16 initializeStyles,
17 resolveBundledStylesSourcePath,
18 resolveInstalledStylesPath,
19 setStylesRuntime,
20 warmStyleThumbnails
21 } from '../styles'
22 import { backfillUserStylePackagesFromDatabase, setStyleDb } from '../styles/catalog'
23 import { applyProxy } from '../utils/proxy'
24 import { configureLogging, scheduleUpdateNotification } from './lifecycle'
25 import { createTray, destroyTray, showTrayHideBalloon } from './tray'
26 import { createMainWindow, showMainWindow } from './window'
27 import { attachWindowControlStateEvents, registerWindowControlHandlers } from './window-controls'
28
29 /** Owns the main-process composition state; `index.ts` only wires Electron lifecycle events. */
30 export class MainApplication {
31 private mainWindow: BrowserWindow | null = null
32 private db: PPTDatabase | null = null
33 private agentManager: AgentManager | null = null
34 private isShuttingDown = false
35 private isTrayEnabled = false
36
37 focusMainWindow(): void {
38 log.info('[app] second instance requested; focusing existing window')
39 showMainWindow(this.mainWindow)
40 }
41
42 async start(): Promise<void> {
43 configureLogging()
44 electronApp.setAppUserModelId('com.ohmyppt.app')
45
46 const dbPath = is.dev ? join(process.cwd(), 'ohmyppt.dev.db') : undefined
47 this.db = new PPTDatabase(dbPath)
48 await this.db.init()
49 configureHtmlThumbnailService(this.db)
50 await this.db.failInterruptedThumbnailTasks()
51 setStyleDb(this.db)
52 log.info('[app] database initialized', {
53 env: is.dev ? 'dev' : 'prod',
54 dbPath: dbPath || 'userData/ohmyppt.db'
55 })
56
57 const installedStylesPath = resolveInstalledStylesPath()
58 const stylesReadyPromise = initializeStyles({
59 bundledSourcePath: resolveBundledStylesSourcePath(),
60 installedRootPath: installedStylesPath,
61 logger: log
62 })
63 .then(async (result) => {
64 await this.db?.syncInstalledStylesToDatabase(installedStylesPath)
65 const userPackageBackfill = await backfillUserStylePackagesFromDatabase(installedStylesPath)
66 const backfill = await this.db?.backfillSessionStyleSnapshots()
67 log.info('[styles] initialized', {
68 installedStylesPath,
69 bundledCount: result.bundledCount,
70 copiedCount: result.copiedCount,
71 failedCount: result.failedCount,
72 userPackageBackfill,
73 snapshotBackfill: backfill
74 })
75 return result
76 })
77 .catch((error) => {
78 log.warn('[styles] initialize failed', {
79 message: error instanceof Error ? error.message : String(error)
80 })
81 throw error
82 })
83 setStylesRuntime({ installedStylesPath, ready: stylesReadyPromise })
84 await stylesReadyPromise
85
86 const installedSkillsPath = resolveInstalledSkillsPath()
87 const skillsReadyPromise = initializeSkills({
88 builtinSourcePath: resolveBuiltinSkillsSourcePath(),
89 installedRootPath: installedSkillsPath,
90 logger: log
91 })
92 .then((result) => {
93 log.info('[skills] initialized', {
94 installedSkillsPath,
95 builtinCount: result.builtinCount,
96 copiedCount: result.copiedCount,
97 skippedCount: result.skippedCount,
98 failedCount: result.failedCount
99 })
100 return result
101 })
102 .catch((error) => {
103 log.warn('[skills] initialize failed', {
104 message: error instanceof Error ? error.message : String(error)
105 })
106 return null
107 })
108 setSkillsRuntime({ installedSkillsPath, ready: skillsReadyPromise })
109
110 this.agentManager = new AgentManager()
111 const window = this.createWindow()
112 window.webContents.on('did-finish-load', () => {
113 void stylesReadyPromise
114 .then(() => this.db?.listStyleRows() || [])
115 .then((styles) => warmStyleThumbnails(installedStylesPath, styles))
116 .catch((error) => {
117 log.warn('[styles] thumbnail warmup failed', {
118 message: error instanceof Error ? error.message : String(error)
119 })
120 })
121 })
122
123 if (process.platform === 'win32') {
124 this.isTrayEnabled = createTray(window)
125 }
126
127 registerLocalAssetProtocol()
128 setupIPC(window, this.db, this.agentManager)
129 registerWindowControlHandlers()
130 scheduleUpdateNotification(window)
131
132 try {
133 const savedSettings = await this.db.getAllSettings()
134 if (typeof savedSettings.proxy_url === 'string' && savedSettings.proxy_url.trim()) {
135 applyProxy(savedSettings.proxy_url.trim())
136 }
137 } catch (proxyError) {
138 log.warn('[app] failed to apply saved proxy', {
139 message: proxyError instanceof Error ? proxyError.message : String(proxyError)
140 })
141 }
142
143 app.on('browser-window-created', (_, createdWindow) => {
144 optimizer.watchWindowShortcuts(createdWindow)
145 })
146 app.on('activate', () => {
147 if (BrowserWindow.getAllWindows().length === 0) this.createWindow()
148 })
149 }
150
151 handleWindowAllClosed(): void {
152 if (process.platform === 'darwin') return
153 if (!this.isTrayEnabled) app.quit()
154 }
155
156 handleBeforeQuit(): void {
157 if (this.isShuttingDown) return
158 this.isShuttingDown = true
159 destroyTray()
160 if (this.db) {
161 void this.db.close().catch((error) => {
162 log.warn('[app] failed to close database on before-quit', {
163 message: error instanceof Error ? error.message : String(error)
164 })
165 })
166 }
167 }
168
169 private createWindow(): BrowserWindow {
170 const window = createMainWindow({
171 isShuttingDown: () => this.isShuttingDown,
172 isTrayEnabled: () => this.isTrayEnabled,
173 onHideToTray: showTrayHideBalloon
174 })
175 attachWindowControlStateEvents(window)
176 this.mainWindow = window
177 return window
178 }
179 }
180
180 lines TYPESCRIPT