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