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