631 lines
22 KiB
TypeScript
631 lines
22 KiB
TypeScript
require("dotenv").config();
|
|
import { app, BrowserWindow, globalShortcut } from "electron";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
import crypto from "crypto";
|
|
import { findUnusedPorts, setupEnv, setUserConfig } from "./utils";
|
|
import { startFastApiServer, startNextJsServer } from "./utils/servers";
|
|
import {
|
|
baseDir,
|
|
ensureDirectoriesExist,
|
|
fastapiDir,
|
|
getAppDataDir,
|
|
getCacheDir,
|
|
getTempDir,
|
|
getUserConfigPath,
|
|
initializeAppPaths,
|
|
isDev,
|
|
localhost,
|
|
nextjsDir,
|
|
resourceBaseDir,
|
|
} from "./utils/constants";
|
|
import { setupIpcHandlers } from "./ipc";
|
|
import { stopActiveExportProcesses } from "./ipc/export_handlers";
|
|
import { getLiteParseRunnerPath } from "./utils/liteparse-check";
|
|
import {
|
|
buildPathWithImageMagick,
|
|
resolveImageMagickRuntime,
|
|
type ImageMagickRuntime,
|
|
} from "./utils/imagemagick-runtime";
|
|
import { startUpdateChecker, stopUpdateChecker } from "./utils/update-checker";
|
|
import {
|
|
addMainBreadcrumb,
|
|
captureMainException,
|
|
initMainSentry,
|
|
setMainSentryRuntimeContext,
|
|
} from "./sentry/main";
|
|
import { installSafeConsole, safeError, safeLog, safeStderrWrite, safeWarn } from "./utils/safe-console";
|
|
import { memorySnapshotMb } from "./utils/memory";
|
|
import {
|
|
isSupportedExternalUrl,
|
|
openExternalUrl,
|
|
showOpenTargetErrorDialog,
|
|
} from "./utils/open-target";
|
|
import {
|
|
finishChromiumCacheRecovery,
|
|
prepareChromiumCacheRecovery,
|
|
type ChromiumCacheRecoveryStatus,
|
|
} from "./utils/chromium-cache-recovery";
|
|
import { resolveLaunchableExportChromiumPath } from "./utils/export-chromium";
|
|
|
|
installSafeConsole();
|
|
|
|
// Local and ad-hoc signed macOS builds otherwise prompt for Keychain access when
|
|
// Chromium initializes encrypted session storage.
|
|
if (process.platform === "darwin") {
|
|
app.commandLine.appendSwitch("use-mock-keychain");
|
|
}
|
|
|
|
// Linux Chromium requires chrome-sandbox to be root-owned mode 4755; unpacked
|
|
// dist/linux-unpacked builds usually lack that. Disable sandbox only when invalid.
|
|
if (process.platform === "linux") {
|
|
try {
|
|
const sandboxPath = path.join(path.dirname(process.execPath), "chrome-sandbox");
|
|
if (fs.existsSync(sandboxPath)) {
|
|
const st = fs.statSync(sandboxPath);
|
|
const hasSetuid = (st.mode & 0o4777) === 0o4755;
|
|
const rootOwned = st.uid === 0;
|
|
if (!(hasSetuid && rootOwned)) {
|
|
app.commandLine.appendSwitch("no-sandbox");
|
|
}
|
|
} else {
|
|
app.commandLine.appendSwitch("no-sandbox");
|
|
}
|
|
} catch {
|
|
app.commandLine.appendSwitch("no-sandbox");
|
|
}
|
|
// Fall back to /tmp instead of shared memory to avoid Chromium crashes
|
|
// on systems where /dev/shm is unavailable/misconfigured.
|
|
app.commandLine.appendSwitch("disable-dev-shm-usage");
|
|
}
|
|
|
|
var win: BrowserWindow | undefined;
|
|
type ManagedServerProcess = Awaited<ReturnType<typeof startFastApiServer>>;
|
|
var fastApiServer: ManagedServerProcess | undefined;
|
|
var nextjsServer: ManagedServerProcess | undefined;
|
|
let isStopping = false;
|
|
|
|
function getLiveMainWindow(): BrowserWindow | undefined {
|
|
if (!win || win.isDestroyed()) {
|
|
return undefined;
|
|
}
|
|
return win;
|
|
}
|
|
|
|
type ProcessGoneDetails = {
|
|
reason?: string;
|
|
type?: string;
|
|
exitCode?: number;
|
|
serviceName?: string;
|
|
name?: string;
|
|
};
|
|
|
|
function profileHash(userDataDir: string): string {
|
|
return crypto.createHash("sha256").update(userDataDir).digest("hex").slice(0, 16);
|
|
}
|
|
|
|
function updateSentryRuntimeContext(cacheRecovery: ChromiumCacheRecoveryStatus): void {
|
|
setMainSentryRuntimeContext({
|
|
profileHash: profileHash(electronAppPaths.userDataDir),
|
|
cacheRecoveryStatus: cacheRecovery.status,
|
|
cacheRecoveryMode: cacheRecovery.mode,
|
|
});
|
|
}
|
|
|
|
function recordProcessGone(kind: "child" | "renderer", details: ProcessGoneDetails): void {
|
|
const reason = details.reason ?? "unknown";
|
|
const data = {
|
|
kind,
|
|
reason,
|
|
type: details.type,
|
|
exitCode: details.exitCode,
|
|
serviceName: details.serviceName,
|
|
name: details.name,
|
|
};
|
|
|
|
addMainBreadcrumb("process", `electron.${kind}_process_gone`, data);
|
|
if (isStopping || reason === "clean-exit") {
|
|
return;
|
|
}
|
|
|
|
captureMainException(new Error(`Electron ${kind} process gone: ${reason}`), data);
|
|
}
|
|
|
|
function resolveExportConverterPath(appRoot: string): string | undefined {
|
|
const pyDir = path.join(appRoot, "resources", "export", "py");
|
|
const candidates = [
|
|
path.join(pyDir, `convert-${process.platform}-${process.arch}`),
|
|
path.join(pyDir, `convert-${process.platform}-${process.arch}.exe`),
|
|
path.join(pyDir, `convert-${process.platform}`),
|
|
path.join(pyDir, `convert-${process.platform}.exe`),
|
|
path.join(pyDir, "convert"),
|
|
path.join(pyDir, "convert.exe"),
|
|
];
|
|
return candidates.find((candidate) => fs.existsSync(candidate));
|
|
}
|
|
|
|
function isDisableAuthEnabledValue(value?: string): boolean {
|
|
const raw = value?.trim().toLowerCase();
|
|
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
}
|
|
|
|
function resolveElectronDisableAuth(): string {
|
|
const raw = (
|
|
process.env.ELECTRON_DISABLE_AUTH ?? process.env.DISABLE_AUTH
|
|
)?.trim().toLowerCase();
|
|
if (!raw) {
|
|
return "true";
|
|
}
|
|
if (["0", "false", "no", "off"].includes(raw)) {
|
|
return "false";
|
|
}
|
|
if (isDisableAuthEnabledValue(raw)) {
|
|
return "true";
|
|
}
|
|
return "true";
|
|
}
|
|
|
|
function buildImageMagickEnv(runtime: ImageMagickRuntime | null): Partial<FastApiEnv> {
|
|
if (!runtime) {
|
|
return {};
|
|
}
|
|
|
|
const pathKey = process.platform === "win32" && process.env.Path !== undefined
|
|
? "Path"
|
|
: "PATH";
|
|
|
|
return {
|
|
IMAGEMAGICK_BINARY: runtime.binaryPath,
|
|
MAGICK_HOME: runtime.homeDir,
|
|
MAGICK_CONFIGURE_PATH: runtime.homeDir,
|
|
[pathKey]: buildPathWithImageMagick(runtime),
|
|
};
|
|
}
|
|
|
|
app.commandLine.appendSwitch('gtk-version', '3');
|
|
|
|
// Work around Chromium/Electron GPU compositor issues that can cause
|
|
// startup white screens on some Linux/driver combinations.
|
|
app.disableHardwareAcceleration();
|
|
|
|
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
|
if (!gotSingleInstanceLock) {
|
|
app.quit();
|
|
}
|
|
|
|
const electronAppPaths = initializeAppPaths();
|
|
const chromiumCacheRecovery = prepareChromiumCacheRecovery(
|
|
electronAppPaths.cacheDir,
|
|
electronAppPaths.userDataDir,
|
|
);
|
|
safeLog("[Presenton] Electron paths initialized:", electronAppPaths);
|
|
|
|
initMainSentry();
|
|
updateSentryRuntimeContext(chromiumCacheRecovery);
|
|
|
|
app.on("child-process-gone", (_event, details) => {
|
|
recordProcessGone("child", details);
|
|
});
|
|
|
|
addMainBreadcrumb("memory", "electron.main.startup", memorySnapshotMb());
|
|
safeLog("[Presenton] Startup memory:", {
|
|
memory: memorySnapshotMb(),
|
|
});
|
|
|
|
const createWindow = () => {
|
|
const mainWindow = new BrowserWindow({
|
|
width: 1280,
|
|
height: 720,
|
|
show: false, // Reveal once the launch screen has painted to avoid a blank flash.
|
|
backgroundColor: "#f3f5ff",
|
|
icon: path.join(resourceBaseDir, "resources/ui/assets/images/presenton_short_filled.png"),
|
|
webPreferences: {
|
|
webSecurity: false,
|
|
// Ensure a known preload path and explicit isolation settings so
|
|
// the `contextBridge` API is exposed reliably to renderer pages.
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
sandbox: false,
|
|
preload: (() => {
|
|
const p = path.join(__dirname, 'preloads/index.js');
|
|
try {
|
|
if (!fs.existsSync(p)) {
|
|
safeWarn(`[Presenton] Preload not found at ${p}`);
|
|
}
|
|
} catch (e) {
|
|
safeWarn('[Presenton] Failed to stat preload path', e);
|
|
}
|
|
return p;
|
|
})(),
|
|
},
|
|
});
|
|
win = mainWindow;
|
|
|
|
mainWindow.on("closed", () => {
|
|
if (win === mainWindow) {
|
|
win = undefined;
|
|
}
|
|
});
|
|
|
|
mainWindow.webContents.on("render-process-gone", (_event, details) => {
|
|
recordProcessGone("renderer", details);
|
|
});
|
|
|
|
// Open external links (e.g. "Download update") in the system browser so the user
|
|
// sees download progress and can manage downloads normally.
|
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
if (!isSupportedExternalUrl(url)) {
|
|
safeWarn("[Presenton] Blocked unsupported window open URL.");
|
|
return { action: "deny" };
|
|
}
|
|
|
|
void openExternalUrl(url)
|
|
.then(async (result) => {
|
|
if (result.success) {
|
|
return;
|
|
}
|
|
|
|
safeWarn(`[Presenton] Failed to open external URL: ${result.message || "Unknown error"}`);
|
|
await showOpenTargetErrorDialog({
|
|
parent: mainWindow,
|
|
title: "Could Not Open Link",
|
|
message: "Presenton could not open this link in your browser.",
|
|
detail: `${result.message || "No application is registered to open this link."}\n\n${url}`,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
safeWarn("[Presenton] Failed to handle external URL open:", error);
|
|
});
|
|
|
|
return { action: "deny" };
|
|
});
|
|
|
|
mainWindow.once("ready-to-show", () => {
|
|
if (mainWindow.isDestroyed()) {
|
|
return;
|
|
}
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
});
|
|
|
|
};
|
|
|
|
function focusMainWindow(): void {
|
|
const mainWindow = getLiveMainWindow();
|
|
if (!mainWindow) {
|
|
return;
|
|
}
|
|
if (mainWindow.isMinimized()) {
|
|
mainWindow.restore();
|
|
}
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
}
|
|
|
|
if (gotSingleInstanceLock) {
|
|
app.on("second-instance", () => {
|
|
focusMainWindow();
|
|
});
|
|
}
|
|
|
|
async function startServers(fastApiPort: number, nextjsPort: number) {
|
|
try {
|
|
const appDataDir = getAppDataDir();
|
|
const tempDir = getTempDir();
|
|
const userConfigPath = getUserConfigPath();
|
|
const disableAuthForElectron = resolveElectronDisableAuth();
|
|
const imageMagickRuntime = resolveImageMagickRuntime();
|
|
const exportPackageRoot = path.join(resourceBaseDir, "resources", "export");
|
|
const exportConverterPath = resolveExportConverterPath(resourceBaseDir);
|
|
const exportChromiumPath = await resolveLaunchableExportChromiumPath();
|
|
const puppeteerCacheDir = path.join(getCacheDir(), "puppeteer");
|
|
const puppeteerTempDir = path.join(tempDir, "puppeteer");
|
|
await Promise.all([
|
|
fs.promises.mkdir(puppeteerCacheDir, { recursive: true }),
|
|
fs.promises.mkdir(puppeteerTempDir, { recursive: true }),
|
|
]);
|
|
if (exportChromiumPath) {
|
|
safeLog("[Presenton] Export Chromium runtime resolved:", exportChromiumPath);
|
|
} else {
|
|
safeWarn(
|
|
"[Presenton] Export Chromium runtime was not found; Template Studio slide previews will fail until Chromium is installed."
|
|
);
|
|
}
|
|
if (imageMagickRuntime) {
|
|
safeLog("[Presenton] ImageMagick runtime resolved:", {
|
|
source: imageMagickRuntime.source,
|
|
binaryPath: imageMagickRuntime.binaryPath,
|
|
homeDir: imageMagickRuntime.homeDir,
|
|
});
|
|
} else {
|
|
safeWarn("[Presenton] ImageMagick runtime was not found; LiteParse image conversion will fail until it is bundled or installed.");
|
|
}
|
|
const fastApi = await startFastApiServer(
|
|
fastapiDir,
|
|
fastApiPort,
|
|
{
|
|
DEBUG: isDev ? "True" : "False",
|
|
CAN_CHANGE_KEYS: process.env.CAN_CHANGE_KEYS,
|
|
LLM: process.env.LLM,
|
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
|
OPENAI_MODEL: process.env.OPENAI_MODEL,
|
|
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
|
|
DEEPSEEK_MODEL: process.env.DEEPSEEK_MODEL,
|
|
DEEPSEEK_BASE_URL: process.env.DEEPSEEK_BASE_URL,
|
|
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
|
|
GOOGLE_MODEL: process.env.GOOGLE_MODEL,
|
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL,
|
|
OLLAMA_URL: process.env.OLLAMA_URL,
|
|
OLLAMA_MODEL: process.env.OLLAMA_MODEL,
|
|
CUSTOM_LLM_URL: process.env.CUSTOM_LLM_URL,
|
|
CUSTOM_LLM_API_KEY: process.env.CUSTOM_LLM_API_KEY,
|
|
CUSTOM_MODEL: process.env.CUSTOM_MODEL,
|
|
BEDROCK_REGION: process.env.BEDROCK_REGION,
|
|
BEDROCK_API_KEY: process.env.BEDROCK_API_KEY,
|
|
BEDROCK_AWS_ACCESS_KEY_ID: process.env.BEDROCK_AWS_ACCESS_KEY_ID,
|
|
BEDROCK_AWS_SECRET_ACCESS_KEY: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY,
|
|
BEDROCK_AWS_SESSION_TOKEN: process.env.BEDROCK_AWS_SESSION_TOKEN,
|
|
BEDROCK_PROFILE_NAME: process.env.BEDROCK_PROFILE_NAME,
|
|
BEDROCK_MODEL: process.env.BEDROCK_MODEL,
|
|
FIREWORKS_API_KEY: process.env.FIREWORKS_API_KEY,
|
|
FIREWORKS_MODEL: process.env.FIREWORKS_MODEL,
|
|
FIREWORKS_BASE_URL: process.env.FIREWORKS_BASE_URL,
|
|
TOGETHER_API_KEY: process.env.TOGETHER_API_KEY,
|
|
TOGETHER_MODEL: process.env.TOGETHER_MODEL,
|
|
TOGETHER_BASE_URL: process.env.TOGETHER_BASE_URL,
|
|
LMSTUDIO_BASE_URL: process.env.LMSTUDIO_BASE_URL,
|
|
LMSTUDIO_API_KEY: process.env.LMSTUDIO_API_KEY,
|
|
LMSTUDIO_MODEL: process.env.LMSTUDIO_MODEL,
|
|
PEXELS_API_KEY: process.env.PEXELS_API_KEY,
|
|
PIXABAY_API_KEY: process.env.PIXABAY_API_KEY,
|
|
IMAGE_PROVIDER: process.env.IMAGE_PROVIDER,
|
|
DISABLE_IMAGE_GENERATION: process.env.DISABLE_IMAGE_GENERATION,
|
|
EXTENDED_REASONING: process.env.EXTENDED_REASONING,
|
|
TOOL_CALLS: process.env.TOOL_CALLS,
|
|
DISABLE_THINKING: process.env.DISABLE_THINKING,
|
|
WEB_GROUNDING: process.env.WEB_GROUNDING,
|
|
WEB_SEARCH_PROVIDER: process.env.WEB_SEARCH_PROVIDER,
|
|
WEB_SEARCH_MAX_RESULTS: process.env.WEB_SEARCH_MAX_RESULTS,
|
|
SEARXNG_BASE_URL: process.env.SEARXNG_BASE_URL,
|
|
TAVILY_API_KEY: process.env.TAVILY_API_KEY,
|
|
EXA_API_KEY: process.env.EXA_API_KEY,
|
|
BRAVE_SEARCH_API_KEY: process.env.BRAVE_SEARCH_API_KEY,
|
|
SERPER_API_KEY: process.env.SERPER_API_KEY,
|
|
DATABASE_URL: process.env.DATABASE_URL,
|
|
DISABLE_ANONYMOUS_TRACKING: process.env.DISABLE_ANONYMOUS_TRACKING,
|
|
COMFYUI_URL: process.env.COMFYUI_URL,
|
|
COMFYUI_WORKFLOW: process.env.COMFYUI_WORKFLOW,
|
|
DALL_E_3_QUALITY: process.env.DALL_E_3_QUALITY,
|
|
GPT_IMAGE_1_5_QUALITY: process.env.GPT_IMAGE_1_5_QUALITY,
|
|
APP_DATA_DIRECTORY: appDataDir,
|
|
TEMP_DIRECTORY: tempDir,
|
|
USER_CONFIG_PATH: userConfigPath,
|
|
MIGRATE_DATABASE_ON_STARTUP: "True",
|
|
DISABLE_AUTH: disableAuthForElectron,
|
|
PRESENTON_ELECTRON: "true",
|
|
...buildImageMagickEnv(imageMagickRuntime),
|
|
LITEPARSE_RUNNER_PATH: getLiteParseRunnerPath(),
|
|
// Use Electron's embedded runtime for LiteParse so parsing does not
|
|
// depend on a system-wide Node installation.
|
|
LITEPARSE_NODE_BINARY: process.execPath,
|
|
ELECTRON_RUN_AS_NODE: "1",
|
|
EXPORT_PACKAGE_ROOT: exportPackageRoot,
|
|
EXPORT_RUNTIME_DIR: exportPackageRoot,
|
|
PUPPETEER_CACHE_DIR: puppeteerCacheDir,
|
|
PUPPETEER_TMP_DIR: puppeteerTempDir,
|
|
...(exportChromiumPath && {
|
|
PUPPETEER_EXECUTABLE_PATH: exportChromiumPath,
|
|
}),
|
|
...(exportConverterPath && {
|
|
BUILT_PYTHON_MODULE_PATH: exportConverterPath,
|
|
}),
|
|
},
|
|
isDev,
|
|
);
|
|
fastApiServer = fastApi;
|
|
await fastApi.ready;
|
|
|
|
const nextjs = await startNextJsServer(
|
|
nextjsDir,
|
|
nextjsPort,
|
|
{
|
|
NEXT_PUBLIC_FAST_API: process.env.NEXT_PUBLIC_FAST_API,
|
|
TEMP_DIRECTORY: process.env.TEMP_DIRECTORY,
|
|
NEXT_PUBLIC_URL: process.env.NEXT_PUBLIC_URL,
|
|
NEXT_PUBLIC_USER_CONFIG_PATH: process.env.NEXT_PUBLIC_USER_CONFIG_PATH,
|
|
USER_CONFIG_PATH: process.env.NEXT_PUBLIC_USER_CONFIG_PATH,
|
|
APP_DATA_DIRECTORY: appDataDir,
|
|
DISABLE_AUTH: disableAuthForElectron,
|
|
EXPORT_PACKAGE_ROOT: exportPackageRoot,
|
|
PRESENTON_APP_ROOT: resourceBaseDir,
|
|
...(exportConverterPath && {
|
|
BUILT_PYTHON_MODULE_PATH: exportConverterPath,
|
|
}),
|
|
},
|
|
isDev,
|
|
);
|
|
nextjsServer = nextjs;
|
|
await nextjs.ready;
|
|
} catch (error) {
|
|
safeError("Server startup error:", error);
|
|
}
|
|
}
|
|
|
|
async function stopServers() {
|
|
const fastApi = fastApiServer;
|
|
const nextjs = nextjsServer;
|
|
fastApiServer = undefined;
|
|
nextjsServer = undefined;
|
|
|
|
await Promise.all([
|
|
fastApi
|
|
? fastApi.stop().catch((error) => safeError("Failed to stop FastAPI:", error))
|
|
: Promise.resolve(),
|
|
nextjs
|
|
? nextjs.stop().catch((error) => safeError("Failed to stop NextJS:", error))
|
|
: Promise.resolve(),
|
|
]);
|
|
}
|
|
|
|
async function forceQuitApp(exitCode = 0) {
|
|
if (isStopping) return;
|
|
isStopping = true;
|
|
globalShortcut.unregisterAll();
|
|
stopUpdateChecker();
|
|
try {
|
|
await stopActiveExportProcesses();
|
|
await stopServers();
|
|
} finally {
|
|
app.exit(exitCode);
|
|
}
|
|
}
|
|
|
|
if (gotSingleInstanceLock) {
|
|
app.whenReady().then(async () => {
|
|
const disableAuthForElectron = resolveElectronDisableAuth();
|
|
process.env.DISABLE_AUTH = disableAuthForElectron;
|
|
process.env.ELECTRON_DISABLE_AUTH = disableAuthForElectron;
|
|
process.env.PRESENTON_ELECTRON = "true";
|
|
|
|
// Ensure all required directories exist before starting
|
|
ensureDirectoriesExist();
|
|
|
|
await finishChromiumCacheRecovery(
|
|
electronAppPaths.userDataDir,
|
|
chromiumCacheRecovery,
|
|
);
|
|
updateSentryRuntimeContext(chromiumCacheRecovery);
|
|
|
|
// Create main window and show the launch page while local servers boot.
|
|
createWindow();
|
|
const initialWindow = getLiveMainWindow();
|
|
if (initialWindow && !initialWindow.webContents.isDestroyed()) {
|
|
void initialWindow
|
|
.loadFile(path.join(resourceBaseDir, "resources/ui/homepage/index.html"))
|
|
.catch((error) => {
|
|
if (!initialWindow.isDestroyed()) {
|
|
safeWarn("[Presenton] Failed to load startup page", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Ensure the launch screen stays visible and focused during the server boot.
|
|
const launchWindow = getLiveMainWindow();
|
|
launchWindow?.show();
|
|
launchWindow?.focus();
|
|
|
|
try {
|
|
setUserConfig({
|
|
CAN_CHANGE_KEYS: process.env.CAN_CHANGE_KEYS,
|
|
LLM: process.env.LLM,
|
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
|
OPENAI_MODEL: process.env.OPENAI_MODEL,
|
|
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
|
|
DEEPSEEK_MODEL: process.env.DEEPSEEK_MODEL,
|
|
DEEPSEEK_BASE_URL: process.env.DEEPSEEK_BASE_URL,
|
|
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
|
|
GOOGLE_MODEL: process.env.GOOGLE_MODEL,
|
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL,
|
|
OLLAMA_URL: process.env.OLLAMA_URL,
|
|
OLLAMA_MODEL: process.env.OLLAMA_MODEL,
|
|
CUSTOM_LLM_URL: process.env.CUSTOM_LLM_URL,
|
|
CUSTOM_LLM_API_KEY: process.env.CUSTOM_LLM_API_KEY,
|
|
CUSTOM_MODEL: process.env.CUSTOM_MODEL,
|
|
BEDROCK_REGION: process.env.BEDROCK_REGION,
|
|
BEDROCK_API_KEY: process.env.BEDROCK_API_KEY,
|
|
BEDROCK_AWS_ACCESS_KEY_ID: process.env.BEDROCK_AWS_ACCESS_KEY_ID,
|
|
BEDROCK_AWS_SECRET_ACCESS_KEY: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY,
|
|
BEDROCK_AWS_SESSION_TOKEN: process.env.BEDROCK_AWS_SESSION_TOKEN,
|
|
BEDROCK_PROFILE_NAME: process.env.BEDROCK_PROFILE_NAME,
|
|
BEDROCK_MODEL: process.env.BEDROCK_MODEL,
|
|
FIREWORKS_API_KEY: process.env.FIREWORKS_API_KEY,
|
|
FIREWORKS_MODEL: process.env.FIREWORKS_MODEL,
|
|
FIREWORKS_BASE_URL: process.env.FIREWORKS_BASE_URL,
|
|
TOGETHER_API_KEY: process.env.TOGETHER_API_KEY,
|
|
TOGETHER_MODEL: process.env.TOGETHER_MODEL,
|
|
TOGETHER_BASE_URL: process.env.TOGETHER_BASE_URL,
|
|
LMSTUDIO_BASE_URL: process.env.LMSTUDIO_BASE_URL,
|
|
LMSTUDIO_API_KEY: process.env.LMSTUDIO_API_KEY,
|
|
LMSTUDIO_MODEL: process.env.LMSTUDIO_MODEL,
|
|
PEXELS_API_KEY: process.env.PEXELS_API_KEY,
|
|
PIXABAY_API_KEY: process.env.PIXABAY_API_KEY,
|
|
IMAGE_PROVIDER: process.env.IMAGE_PROVIDER,
|
|
DISABLE_IMAGE_GENERATION: process.env.DISABLE_IMAGE_GENERATION,
|
|
EXTENDED_REASONING: process.env.EXTENDED_REASONING,
|
|
TOOL_CALLS: process.env.TOOL_CALLS,
|
|
DISABLE_THINKING: process.env.DISABLE_THINKING,
|
|
WEB_GROUNDING: process.env.WEB_GROUNDING,
|
|
WEB_SEARCH_PROVIDER: process.env.WEB_SEARCH_PROVIDER,
|
|
WEB_SEARCH_MAX_RESULTS: process.env.WEB_SEARCH_MAX_RESULTS,
|
|
SEARXNG_BASE_URL: process.env.SEARXNG_BASE_URL,
|
|
TAVILY_API_KEY: process.env.TAVILY_API_KEY,
|
|
EXA_API_KEY: process.env.EXA_API_KEY,
|
|
BRAVE_SEARCH_API_KEY: process.env.BRAVE_SEARCH_API_KEY,
|
|
SERPER_API_KEY: process.env.SERPER_API_KEY,
|
|
DATABASE_URL: process.env.DATABASE_URL,
|
|
DISABLE_ANONYMOUS_TRACKING: process.env.DISABLE_ANONYMOUS_TRACKING,
|
|
COMFYUI_URL: process.env.COMFYUI_URL,
|
|
COMFYUI_WORKFLOW: process.env.COMFYUI_WORKFLOW,
|
|
DALL_E_3_QUALITY: process.env.DALL_E_3_QUALITY,
|
|
GPT_IMAGE_1_5_QUALITY: process.env.GPT_IMAGE_1_5_QUALITY,
|
|
})
|
|
} catch (error) {
|
|
safeWarn("[Presenton] Failed to persist startup user config", error);
|
|
}
|
|
|
|
const [fastApiPort, nextjsPort] = await findUnusedPorts();
|
|
safeLog(`FastAPI port: ${fastApiPort}, NextJS port: ${nextjsPort}`);
|
|
|
|
//? Setup environment variables to be used in the preloads
|
|
setupEnv(fastApiPort, nextjsPort);
|
|
setupIpcHandlers();
|
|
|
|
await startServers(fastApiPort, nextjsPort);
|
|
if (isStopping) {
|
|
return;
|
|
}
|
|
const mainWindow = getLiveMainWindow();
|
|
if (!mainWindow || mainWindow.webContents.isDestroyed()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const appPath = isDisableAuthEnabledValue(process.env.DISABLE_AUTH)
|
|
? "/upload"
|
|
: "";
|
|
await mainWindow.loadURL(`${localhost}:${nextjsPort}${appPath}`);
|
|
} catch (error) {
|
|
if (mainWindow.isDestroyed()) {
|
|
return;
|
|
}
|
|
safeWarn("[Presenton] Failed to load application URL", error);
|
|
return;
|
|
}
|
|
|
|
// Begin polling the version server for available updates
|
|
const updateWindow = getLiveMainWindow();
|
|
if (updateWindow && !updateWindow.webContents.isDestroyed()) {
|
|
safeStderrWrite("[Presenton] Starting update checker...\n");
|
|
startUpdateChecker(updateWindow);
|
|
}
|
|
});
|
|
|
|
app.on("window-all-closed", async () => {
|
|
await forceQuitApp(0);
|
|
});
|
|
|
|
app.on("before-quit", async (event) => {
|
|
if (isStopping) return;
|
|
event.preventDefault();
|
|
await forceQuitApp(0);
|
|
});
|
|
|
|
app.on("will-quit", async (event) => {
|
|
if (isStopping) return;
|
|
event.preventDefault();
|
|
await forceQuitApp(0);
|
|
});
|
|
}
|