669 lines
17 KiB
JavaScript
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();
|
|
});
|