import { app, BrowserWindow, ipcMain, Menu, nativeTheme, shell, utilityProcess, } from 'electron'; import electronUpdater from 'electron-updater'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const { autoUpdater } = electronUpdater; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const brandName = 'TrueGrowth'; const rendererUrl = process.env.VITE_DEV_SERVER_URL; const localApiPort = Number(process.env.TRUEGROWTH_LOCAL_API_PORT || 48177); const updateFeedUrl = process.env.TRUEGROWTH_UPDATE_FEED_URL || 'https://truegrowth.benchu.cloud/desktop/stable/'; const updateCheckDelayMs = Number( process.env.TRUEGROWTH_UPDATE_CHECK_DELAY_MS || 15000 ); const updateCheckIntervalMs = Number( process.env.TRUEGROWTH_UPDATE_CHECK_INTERVAL_MS || 4 * 60 * 60 * 1000 ); const userDataPath = process.env.TRUEGROWTH_ELECTRON_USER_DATA || path.join(app.getPath('appData'), brandName); const isDev = Boolean(rendererUrl); const appRoot = app.isPackaged ? app.getAppPath() : path.resolve(__dirname, '../..'); const brandIconPath = resolveAppPath( 'apps/electron/assets/truegrowth-dock-icon.png' ); let mainWindow = null; let localApi = null; let updateCheckTimer = null; let updateCheckInFlight = null; let updateDownloadInFlight = null; let updateState = null; function readAppVersion() { try { const packageInfo = JSON.parse( fs.readFileSync(resolveAppPath('package.json'), 'utf8') ); return packageInfo.version || app.getVersion(); } catch { return app.getVersion(); } } const appVersion = readAppVersion(); updateState = createInitialUpdateState(); function resolveAppPath(...segments) { return path.join(appRoot, ...segments); } function createInitialUpdateState() { return { appVersion: appVersion || app.getVersion(), available: false, autoDownload: false, canCheck: true, canDownload: false, canInstall: false, channelUrl: updateFeedUrl, downloaded: false, downloading: false, error: '', feedReady: false, isPackaged: app.isPackaged, lastCheckedAt: null, notes: '', percent: 0, platform: process.platform, status: 'idle', updateVersion: null, }; } function normalizeReleaseNotes(releaseNotes) { if (Array.isArray(releaseNotes)) { return releaseNotes .map((item) => { if (typeof item === 'string') { return item; } if (item && typeof item === 'object') { return String(item.note || item.notes || item.version || ''); } return ''; }) .filter(Boolean) .join('\n'); } if (typeof releaseNotes === 'string') { return releaseNotes; } return ''; } function updateUpdaterState(patch) { updateState = { ...updateState, ...patch, appVersion: appVersion || app.getVersion(), channelUrl: updateFeedUrl, isPackaged: app.isPackaged, platform: process.platform, }; broadcastUpdaterState(); return updateState; } function broadcastUpdaterState() { BrowserWindow.getAllWindows().forEach((window) => { if (!window.isDestroyed()) { window.webContents.send('truegrowth-update:state', updateState); } }); } function configureAutoUpdater() { autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = false; autoUpdater.allowDowngrade = false; autoUpdater.allowPrerelease = false; autoUpdater.logger = console; autoUpdater.setFeedURL({ provider: 'generic', url: updateFeedUrl, }); updateUpdaterState({ autoDownload: autoUpdater.autoDownload, feedReady: true, }); autoUpdater.on('checking-for-update', () => { updateUpdaterState({ canCheck: false, error: '', status: 'checking', }); }); autoUpdater.on('update-not-available', () => { updateUpdaterState({ available: false, canCheck: true, canDownload: false, canInstall: false, downloaded: false, downloading: false, error: '', lastCheckedAt: Date.now(), percent: 0, status: 'not-available', updateVersion: null, }); }); autoUpdater.on('update-available', (info) => { updateUpdaterState({ available: true, canCheck: true, canDownload: true, canInstall: false, downloaded: false, downloading: false, error: '', lastCheckedAt: Date.now(), notes: normalizeReleaseNotes(info?.releaseNotes), percent: 0, status: 'available', updateVersion: info?.version || null, }); }); autoUpdater.on('download-progress', (progress) => { updateUpdaterState({ available: true, canCheck: false, canDownload: false, canInstall: false, downloaded: false, downloading: true, error: '', percent: Math.max(0, Math.min(100, Number(progress?.percent || 0))), status: 'downloading', }); }); autoUpdater.on('update-downloaded', (info) => { updateDownloadInFlight = null; updateUpdaterState({ available: true, canCheck: true, canDownload: false, canInstall: true, downloaded: true, downloading: false, error: '', notes: normalizeReleaseNotes(info?.releaseNotes) || updateState.notes, percent: 100, status: 'downloaded', updateVersion: info?.version || updateState.updateVersion, }); }); autoUpdater.on('error', (error) => { updateCheckInFlight = null; updateDownloadInFlight = null; updateUpdaterState({ canCheck: true, canDownload: Boolean(updateState.available && !updateState.downloaded), downloading: false, error: error instanceof Error ? error.message : String(error || '更新检查失败'), status: 'error', }); }); } async function checkForDesktopUpdate({ manual = false } = {}) { if (!app.isPackaged && !process.env.TRUEGROWTH_ALLOW_DEV_UPDATES) { return updateUpdaterState({ canCheck: true, error: manual ? '开发模式下不会连接正式更新通道。' : '', feedReady: true, status: manual ? 'dev-disabled' : 'idle', }); } if (updateCheckInFlight) { return updateCheckInFlight; } updateCheckInFlight = autoUpdater .checkForUpdates() .then(() => updateState) .finally(() => { updateCheckInFlight = null; }); return updateCheckInFlight; } async function downloadDesktopUpdate() { if (!app.isPackaged && !process.env.TRUEGROWTH_ALLOW_DEV_UPDATES) { return updateUpdaterState({ error: '开发模式下不会下载正式更新包。', status: 'dev-disabled', }); } if (!updateState.available) { return updateUpdaterState({ error: '当前没有可下载的新版本。', status: 'error', }); } if (updateDownloadInFlight) { return updateDownloadInFlight; } updateDownloadInFlight = autoUpdater .downloadUpdate() .then(() => updateState) .finally(() => { if (updateState.status !== 'downloaded') { updateDownloadInFlight = null; } }); updateUpdaterState({ canCheck: false, canDownload: false, downloading: true, error: '', status: 'downloading', }); return updateDownloadInFlight; } function installDesktopUpdate() { if (!updateState.downloaded) { return updateUpdaterState({ error: '更新包尚未下载完成。', status: 'error', }); } updateUpdaterState({ canCheck: false, canDownload: false, canInstall: false, status: 'installing', }); autoUpdater.quitAndInstall(false, true); return updateState; } function openDesktopUpdateChannel() { void shell.openExternal(updateFeedUrl); return updateState; } function installUpdateIpc() { ipcMain.handle('truegrowth-update:get-state', () => updateState); ipcMain.handle('truegrowth-update:check', () => checkForDesktopUpdate({ manual: true }) ); ipcMain.handle('truegrowth-update:download', () => downloadDesktopUpdate()); ipcMain.handle('truegrowth-update:install', () => installDesktopUpdate()); ipcMain.handle('truegrowth-update:open-channel', () => openDesktopUpdateChannel() ); } function scheduleDesktopUpdateChecks() { if (updateCheckTimer) { clearInterval(updateCheckTimer); updateCheckTimer = null; } setTimeout(() => { void checkForDesktopUpdate().catch(() => undefined); }, Math.max(0, updateCheckDelayMs)); updateCheckTimer = setInterval(() => { void checkForDesktopUpdate().catch(() => undefined); }, Math.max(60_000, updateCheckIntervalMs)); } function createDesktopLocalRendererEntry(indexHtmlPath) { const html = fs.readFileSync(indexHtmlPath, 'utf8'); const appAssetPattern = /https:\/\/cdn\.jsdelivr\.net\/npm\/aitu-app@[^/"'>\s]+\/([^"'<>\s]+)/g; return html.replace(appAssetPattern, (_url, assetPath) => `./${assetPath}`); } process.title = brandName; app.setName(brandName); app.setAppUserModelId('ai.truegrowth.desktop'); app.setPath('userData', userDataPath); process.env.TRUEGROWTH_ELECTRON_USER_DATA = userDataPath; process.env.TRUEGROWTH_RUNTIME_STATE_DIR = process.env.TRUEGROWTH_RUNTIME_STATE_DIR || path.join(userDataPath, 'runtime'); if (!isDev) { process.env.TRUEGROWTH_DESKTOP_PACKAGED = process.env.TRUEGROWTH_DESKTOP_PACKAGED || '1'; } process.env.TRUEGROWTH_DESKTOP_APP_ROOT = process.env.TRUEGROWTH_DESKTOP_APP_ROOT || appRoot; process.env.TRUEGROWTH_DESKTOP_RESOURCE_DIR = process.env.TRUEGROWTH_DESKTOP_RESOURCE_DIR || (app.isPackaged ? process.resourcesPath : appRoot); const hasSingleInstanceLock = app.requestSingleInstanceLock({ rendererUrl, userDataPath, }); if (!hasSingleInstanceLock) { app.quit(); process.exit(0); } app.on('second-instance', () => { if (!mainWindow) { return; } if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.focus(); mainWindow.show(); }); function getWindowState(window) { return { isFullScreen: window?.isFullScreen?.() ?? false, isMaximized: window?.isMaximized?.() ?? false, platform: process.platform, }; } function getSystemThemeState() { const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; return { theme, shouldUseDarkColors: theme === 'dark', source: 'system', }; } function sendWindowState(window) { if (!window || window.isDestroyed()) { return; } window.webContents.send('truegrowth-window:state', getWindowState(window)); } function sendSystemThemeState(window) { if (!window || window.isDestroyed()) { return; } const systemTheme = getSystemThemeState(); window.setBackgroundColor( systemTheme.theme === 'dark' ? '#060606' : '#f7f7f5' ); window.webContents.send('truegrowth-theme:system', systemTheme); } function getWindowFromIpcEvent(event) { const window = BrowserWindow.fromWebContents(event.sender); return window && !window.isDestroyed() ? window : null; } function installWindowControlsIpc() { ipcMain.handle('truegrowth-window:minimize', (event) => { const window = getWindowFromIpcEvent(event); window?.minimize(); return window ? getWindowState(window) : getWindowState(mainWindow); }); ipcMain.handle('truegrowth-window:toggle-maximize', (event) => { const window = getWindowFromIpcEvent(event); if (!window) { return getWindowState(mainWindow); } if (window.isFullScreen()) { window.setFullScreen(false); } else if (window.isMaximized()) { window.unmaximize(); } else { window.maximize(); } return getWindowState(window); }); ipcMain.handle('truegrowth-window:close', (event) => { const window = getWindowFromIpcEvent(event); window?.close(); return { closed: Boolean(window) }; }); ipcMain.handle('truegrowth-window:is-maximized', (event) => { const window = getWindowFromIpcEvent(event); return getWindowState(window || mainWindow); }); } function installThemeIpc() { ipcMain.handle('truegrowth-theme:get-system', () => getSystemThemeState()); nativeTheme.on('updated', () => { BrowserWindow.getAllWindows().forEach(sendSystemThemeState); }); } function installApplicationMenu() { const editMenu = [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }, ]; const viewMenu = [ { role: 'reload' }, { role: 'forceReload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' }, ]; const windowMenu = [ { role: 'minimize' }, { role: 'zoom' }, ...(process.platform === 'darwin' ? [{ type: 'separator' }, { role: 'front' }] : [{ role: 'close' }]), ]; const helpMenu = [ { label: 'Check for Updates', click: () => { void checkForDesktopUpdate({ manual: true }); }, }, { label: 'Open Update Channel', click: () => { openDesktopUpdateChannel(); }, }, ]; const template = [ ...(process.platform === 'darwin' ? [ { label: brandName, submenu: [ { role: 'about', label: `About ${brandName}` }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide', label: `Hide ${brandName}` }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit', label: `Quit ${brandName}` }, ], }, ] : []), { label: 'File', submenu: [ process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' }, ], }, { label: 'Edit', submenu: editMenu }, { label: 'View', submenu: viewMenu }, { label: 'Window', submenu: windowMenu }, { label: 'Help', submenu: helpMenu }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } function createWindow() { const initialSystemTheme = getSystemThemeState().theme; mainWindow = new BrowserWindow({ width: 1440, height: 920, minWidth: 1180, minHeight: 760, title: brandName, icon: brandIconPath, frame: false, autoHideMenuBar: true, backgroundColor: initialSystemTheme === 'dark' ? '#060606' : '#f7f7f5', webPreferences: { preload: resolveAppPath('apps/electron/preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: false, }, }); if (process.platform !== 'darwin') { mainWindow.setMenuBarVisibility(false); } mainWindow.on('closed', () => { mainWindow = null; }); [ 'enter-full-screen', 'leave-full-screen', 'maximize', 'resize', 'restore', 'unmaximize', ].forEach((eventName) => { mainWindow.on(eventName, () => sendWindowState(mainWindow)); }); mainWindow.webContents.once('did-finish-load', () => { sendWindowState(mainWindow); sendSystemThemeState(mainWindow); broadcastUpdaterState(); }); mainWindow.webContents.setWindowOpenHandler(({ url }) => { void shell.openExternal(url); return { action: 'deny' }; }); if (isDev && rendererUrl) { void mainWindow.loadURL(rendererUrl); return; } const indexHtmlPath = resolveAppPath('dist/apps/web/index.html'); const entryHtml = createDesktopLocalRendererEntry(indexHtmlPath); void mainWindow.loadURL( `data:text/html;charset=utf-8,${encodeURIComponent(entryHtml)}`, { baseURLForDataURL: `${pathToFileURL( path.dirname(indexHtmlPath) ).toString()}/`, } ); } async function startLocalApi() { const { createLocalApiServer } = await import( pathToFileURL(resolveAppPath('scripts/truegrowth-local-api.mjs')).toString() ); localApi = createLocalApiServer({ utilityProcess }); localApi.on('error', (error) => { if (error?.code === 'EADDRINUSE') { console.log( `TrueGrowth local API already available at http://127.0.0.1:${localApiPort}` ); return; } throw error; }); localApi.listen(localApiPort, '127.0.0.1', () => { console.log( `TrueGrowth local API listening at http://127.0.0.1:${localApiPort}` ); }); } app.whenReady().then(async () => { configureAutoUpdater(); installApplicationMenu(); installWindowControlsIpc(); installThemeIpc(); installUpdateIpc(); if (process.platform === 'darwin' && app.dock) { app.dock.setIcon(brandIconPath); } app.setAboutPanelOptions({ applicationName: brandName, applicationVersion: appVersion, iconPath: brandIconPath, copyright: `Copyright © ${new Date().getFullYear()} TrueGrowth`, }); await startLocalApi(); createWindow(); scheduleDesktopUpdateChecks(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('before-quit', () => { if (updateCheckTimer) { clearInterval(updateCheckTimer); updateCheckTimer = null; } localApi?.close(); });