Files
TrueGrowth/apps/electron/main.mjs
jiam 5119ac0ef8
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled
Sync latest TrueGrowth updates
2026-07-08 02:03:18 +08:00

669 lines
17 KiB
JavaScript

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();
});