Files
AstrBot/desktop/main.js
T
エイカク d35771f97d fix: stabilize packaged runtime pip/ssl behavior and mac font fallback (#5007)
* fix: patch pip distlib finder for frozen electron runtime

* fix: use certifi CA bundle for runtime SSL requests

* fix: configure certifi CA before core imports

* fix: improve mac font fallback for dashboard text

* fix: harden frozen pip patch and unify TLS connector

* refactor: centralize dashboard CJK font fallback stacks

* perf: reuse TLS context and avoid repeated frozen pip patch

* refactor: bootstrap TLS setup before core imports

* fix: use async confirm dialog for provider deletions

* fix: replace native confirm dialogs in dashboard

- Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback.

- Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper.

- Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron.

* fix: capture runtime bootstrap logs after logger init

- Add bootstrap record buffer in runtime_bootstrap for early TLS patch logs before logger is ready.

- Flush buffered bootstrap logs to astrbot logger at process startup in main.py.

- Include concrete exception details for TLS bootstrap failures to improve diagnosis.

* fix: harden runtime bootstrap and unify confirm handling

- Simplify bootstrap log buffering and add a public initialize hook for non-main startup paths.

- Guard aiohttp TLS patching with feature/type checks and keep graceful fallback when internals are unavailable.

- Standardize dashboard confirmation flow via shared confirm helpers across composition and options API components.

* refactor: simplify runtime tls bootstrap and tighten confirm typing

* refactor: align ssl helper namespace and confirm usage
2026-02-10 16:42:43 +09:00

396 lines
9.1 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const {
app,
BrowserWindow,
Menu,
Tray,
nativeImage,
shell,
dialog,
ipcMain,
} = require('electron');
const { BackendManager } = require('./lib/backend-manager');
const { loadDashboard } = require('./lib/dashboard-loader');
const { createElectronLogger } = require('./lib/electron-logger');
const { createLocaleService } = require('./lib/locale-service');
const { loadStartupScreen } = require('./lib/startup-screen');
const isMac = process.platform === 'darwin';
const dashboardTimeoutMsParsed = Number.parseInt(
process.env.ASTRBOT_DASHBOARD_TIMEOUT_MS || '20000',
10,
);
const dashboardTimeoutMs = Number.isFinite(dashboardTimeoutMsParsed)
? dashboardTimeoutMsParsed
: 20000;
let mainWindow = null;
let tray = null;
let isQuitting = false;
let quitInProgress = false;
let backendManager = null;
app.commandLine.appendSwitch('disable-http-cache');
const { logElectron } = createElectronLogger({
app,
getRootDir: () => (backendManager ? backendManager.getRootDir() : null),
});
backendManager = new BackendManager({
app,
baseDir: __dirname,
log: logElectron,
shouldSkipStart: () => isQuitting || quitInProgress,
});
const localeService = createLocaleService({
app,
getRootDir: () => backendManager.getRootDir(),
});
function getAssetPath(filename) {
if (app.isPackaged) {
const packaged = path.join(process.resourcesPath, 'assets', filename);
if (fs.existsSync(packaged)) {
return packaged;
}
}
return path.join(__dirname, 'assets', filename);
}
function loadImageSafe(imagePath) {
try {
const image = nativeImage.createFromPath(imagePath);
if (!image.isEmpty()) {
return image;
}
} catch {}
return nativeImage.createEmpty();
}
function showWindow() {
if (!mainWindow) {
return;
}
mainWindow.show();
mainWindow.focus();
updateTrayMenu();
}
function toggleWindow() {
if (!mainWindow) {
return;
}
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
updateTrayMenu();
}
function updateTrayMenu() {
if (!tray || !mainWindow) {
return;
}
const shellTexts = localeService.getShellTexts(
localeService.resolveStartupLocale(),
);
const isVisible = mainWindow.isVisible();
const contextMenu = Menu.buildFromTemplate([
{
label: isVisible ? shellTexts.trayHide : shellTexts.trayShow,
click: () => toggleWindow(),
},
{
label: shellTexts.trayReload,
click: () => {
if (mainWindow) {
mainWindow.reload();
}
},
},
{ type: 'separator' },
{
label: shellTexts.trayQuit,
click: () => app.quit(),
},
]);
tray.setContextMenu(contextMenu);
}
function createTray() {
const traySize = isMac ? 18 : 16;
const trayPath = getAssetPath('tray.png');
let trayImage = loadImageSafe(trayPath);
if (trayImage.isEmpty()) {
trayImage = loadImageSafe(getAssetPath('icon.png'));
}
if (!trayImage.isEmpty()) {
trayImage = trayImage.resize({ width: traySize, height: traySize });
if (isMac) {
trayImage.setTemplateImage(true);
}
tray = new Tray(trayImage);
} else {
tray = new Tray(nativeImage.createEmpty());
}
tray.setToolTip('AstrBot');
tray.on('click', () => toggleWindow());
updateTrayMenu();
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 980,
minHeight: 680,
show: false,
backgroundColor: '#f9fafc',
autoHideMenuBar: !isMac,
icon: getAssetPath('icon.png'),
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: path.join(__dirname, 'preload.js'),
...(isMac
? {
defaultFontFamily: {
standard: 'PingFang SC',
sansSerif: 'PingFang SC',
serif: 'Songti SC',
monospace: 'SF Mono',
},
}
: {}),
},
});
mainWindow.on('close', (event) => {
if (isQuitting) {
return;
}
event.preventDefault();
mainWindow.hide();
});
mainWindow.on('minimize', (event) => {
event.preventDefault();
mainWindow.hide();
});
mainWindow.on('show', () => updateTrayMenu());
mainWindow.on('hide', () => updateTrayMenu());
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
mainWindow.webContents.on(
'did-fail-load',
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
if (!isMainFrame) {
return;
}
logElectron(
`did-fail-load main-frame code=${errorCode} desc=${errorDescription} url=${validatedURL}`,
);
},
);
mainWindow.webContents.on('did-finish-load', () => {
const currentUrl = mainWindow.webContents.getURL();
logElectron(`did-finish-load url=${currentUrl}`);
if (currentUrl.startsWith(backendManager.getBackendUrl())) {
void localeService.persistLocaleFromDashboard(
mainWindow,
backendManager.getBackendUrl(),
);
}
});
mainWindow.webContents.on('render-process-gone', (_event, details) => {
logElectron(
`render-process-gone reason=${details.reason} exitCode=${details.exitCode}`,
);
});
mainWindow.webContents.on(
'console-message',
(_event, level, message, line, sourceId) => {
if (level >= 2) {
logElectron(
`renderer-console level=${level} source=${sourceId}:${line} message=${message}`,
);
}
},
);
return mainWindow;
}
function registerIpcHandlers() {
ipcMain.handle('astrbot-desktop:is-electron-runtime', async () => true);
ipcMain.handle('astrbot-desktop:get-backend-state', async () => {
return backendManager.getState();
});
ipcMain.handle('astrbot-desktop:restart-backend', async () => {
return backendManager.restartBackend();
});
ipcMain.handle('astrbot-desktop:stop-backend', async () => {
return backendManager.stopBackendForIpc();
});
}
async function startDesktopFlow() {
createWindow();
createTray();
try {
const startupTexts = localeService.getStartupTexts(
localeService.resolveStartupLocale(),
);
await loadStartupScreen(mainWindow, {
getAssetPath,
startupTexts,
});
} catch (error) {
logElectron(
`failed to load startup screen: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
showWindow();
const ready = await backendManager.ensureBackend();
if (isQuitting) {
return;
}
if (!ready) {
const shellTexts = localeService.getShellTexts(
localeService.resolveStartupLocale(),
);
const backendLogPath = backendManager.getBackendLogPath();
const detailLines = [];
const startupFailureReason = backendManager.getStartupFailureReason();
if (startupFailureReason) {
detailLines.push(
`${shellTexts.startupFailReasonPrefix}: ${startupFailureReason}`,
);
}
detailLines.push(shellTexts.startupFailAction);
if (backendLogPath) {
detailLines.push(`${shellTexts.startupFailLogPrefix}: ${backendLogPath}`);
}
await dialog.showMessageBox({
type: 'error',
title: shellTexts.startupFailTitle,
message: shellTexts.startupFailMessage,
detail: detailLines.join('\n'),
});
isQuitting = true;
app.quit();
return;
}
try {
await loadDashboard(
mainWindow,
backendManager.getBackendUrl(),
dashboardTimeoutMs,
);
showWindow();
} catch (error) {
const shellTexts = localeService.getShellTexts(
localeService.resolveStartupLocale(),
);
await dialog.showMessageBox({
type: 'error',
title: shellTexts.dashboardFailTitle,
message: shellTexts.dashboardFailMessage,
detail: error instanceof Error ? error.message : String(error),
});
isQuitting = true;
app.quit();
}
}
registerIpcHandlers();
app.setAppUserModelId('com.astrbot.desktop');
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
} else {
app.on('second-instance', () => {
showWindow();
});
}
app.on('before-quit', (event) => {
if (quitInProgress) {
event.preventDefault();
return;
}
event.preventDefault();
quitInProgress = true;
isQuitting = true;
logElectron('before-quit received, stopping backend.');
localeService
.persistLocaleFromDashboard(mainWindow, backendManager.getBackendUrl())
.catch(() => {})
.then(() =>
backendManager.stopManagedBackend().catch((error) => {
logElectron(
`stopBackend failed: ${
error instanceof Error ? error.message : String(error)
}`,
);
}),
)
.finally(() => {
logElectron('Backend stop finished, exiting app.');
app.exit(0);
});
});
app.whenReady().then(async () => {
if (isMac && app.dock) {
const dockIcon = getAssetPath('icon.png');
if (fs.existsSync(dockIcon)) {
app.dock.setIcon(dockIcon);
}
}
await startDesktopFlow();
});
app.on('activate', () => {
if (mainWindow) {
showWindow();
}
});
app.on('window-all-closed', () => {
if (!isMac) {
app.quit();
}
});