feat: add system autostart and single-instance support

This commit is contained in:
Draculabo committed 2026-01-28 13:57:18 +08:00
1 parent dd9e84a0db
commit ea51253d58
6 files changed
+249 -4

No files matched your search

+2
View File
@@ -1,5 +1,6 @@
import { AppConfig } from '../../types/config';
import { ConfigManager } from './manager';
import { syncAutoStart } from '../../utils/autoStart';
export function loadConfig(): AppConfig {
return ConfigManager.loadConfig();
@@ -10,4 +11,5 @@ export function saveConfig(config: AppConfig): void {
// Logic to update Tray if language changes
// For now just save
ConfigManager.saveConfig(config);
syncAutoStart(config);
}
+16
View File
@@ -100,6 +100,14 @@ i18n
auto_sync: 'Auto Sync Current Account',
auto_sync_desc: 'Periodically sync active account information',
},
startup: {
title: 'Startup',
description: 'Control application launch behavior at system startup.',
auto_startup: 'Start with system',
auto_startup_desc: 'Launch at sign-in and keep the app in the system tray',
macos_hint:
'macOS requires a signed app for Login Items to work. If auto-start fails, please sign the app or enable it manually in System Settings.',
},
proxy: {
title: 'Upstream Proxy',
description: 'Configure a proxy for outgoing requests to Google/Gemini APIs.',
@@ -391,6 +399,14 @@ i18n
auto_sync: '自动获取当前账号',
auto_sync_desc: '定期自动同步当前活跃账号信息',
},
startup: {
title: '启动',
description: '控制系统启动时的应用行为。',
auto_startup: '随系统启动',
auto_startup_desc: '登录时自动启动并保持在托盘里',
macos_hint:
'macOS 需要应用已签名才可生效。如自启动失败,请自行签名或在系统设置中手动启用。',
},
proxy: {
title: '上游代理',
description: '为发往 Google/Gemini API 的请求配置代理。',
+56 -4
View File
@@ -18,6 +18,8 @@ import { bootstrapNestServer, stopNestServer } from './server/main';
import { initTray, setTrayLanguage, destroyTray } from './ipc/tray/handler';
import { rpcHandler } from './ipc/handler';
import { ConfigManager } from './ipc/config/manager';
import { AppConfig } from './types/config';
import { isAutoStartLaunch, syncAutoStart } from './utils/autoStart';
const packetLogPath = path.join(app.getPath('userData'), 'orpc_packets.log');
@@ -63,6 +65,25 @@ const inDevelopment = process.env.NODE_ENV === 'development';
let globalMainWindow: BrowserWindow | null = null;
// let tray: Tray | null = null; // Moved to tray/handler.ts
let isQuitting = false;
let startupConfig: AppConfig | null = null;
let shouldStartHidden = false;
const gotSingleInstanceLock = app.requestSingleInstanceLock();
if (!gotSingleInstanceLock) {
app.quit();
} else {
app.on('second-instance', () => {
logger.info('Second instance detected, focusing existing window');
if (app.isReady()) {
createWindow({ startHidden: false });
return;
}
app.whenReady().then(() => {
createWindow({ startHidden: false });
});
});
}
process.on('exit', (code) => {
logger.info(`Process exit event triggered with code: ${code}`);
@@ -75,7 +96,22 @@ process.on('before-exit', (code) => {
// let tray: Tray | null = null; // Moved to tray/handler.ts
function createWindow() {
function createWindow({ startHidden }: { startHidden: boolean }) {
if (globalMainWindow && !globalMainWindow.isDestroyed()) {
if (startHidden) {
globalMainWindow.hide();
return;
}
if (globalMainWindow.isMinimized()) {
globalMainWindow.restore();
}
if (!globalMainWindow.isVisible()) {
globalMainWindow.show();
}
globalMainWindow.focus();
return;
}
logger.info('createWindow: start');
const preload = path.join(__dirname, 'preload.js');
logger.info(`createWindow: preload path: ${preload}`);
@@ -84,6 +120,7 @@ function createWindow() {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
show: !startHidden,
autoHideMenuBar: true,
webPreferences: {
devTools: inDevelopment,
@@ -99,6 +136,10 @@ function createWindow() {
});
globalMainWindow = mainWindow;
logger.info('createWindow: BrowserWindow instance created');
if (startHidden) {
mainWindow.hide();
logger.info('createWindow: startHidden enabled, window hidden');
}
logger.info('createWindow: setting main window in ipcContext');
ipcContext.setMainWindow(mainWindow);
@@ -126,6 +167,7 @@ function createWindow() {
mainWindow.on('closed', () => {
logger.info('Window closed event triggered');
globalMainWindow = null;
});
mainWindow.webContents.on('render-process-gone', (event, details) => {
@@ -241,13 +283,23 @@ app
logger.info('Step: Initialize Antigravity DB (WAL Mode)');
initDatabase();
})
.then(() => {
logger.info('Step: Load Config');
const config = ConfigManager.loadConfig();
startupConfig = config;
syncAutoStart(config);
shouldStartHidden = isAutoStartLaunch() && config.auto_startup;
if (shouldStartHidden) {
logger.info('Startup: Auto-start detected, window will start hidden');
}
})
.then(() => {
logger.info('Step: setupORPC');
return setupORPC();
})
.then(async () => {
logger.info('Step: createWindow');
await createWindow();
await createWindow({ startHidden: shouldStartHidden });
})
.then(() => {
logger.info('Step: installExtensions (SKIPPED)');
@@ -264,7 +316,7 @@ app
AuthServer.start();
// Gateway Server (NestJS) - auto-start if enabled
const config = ConfigManager.loadConfig();
const config = startupConfig || ConfigManager.loadConfig();
if (config.proxy?.auto_start) {
const port = config.proxy?.port || 8045;
// Default to a valid ProxyConfig object if null, although loadConfig ensures defaults
@@ -306,7 +358,7 @@ app.on('window-all-closed', () => {
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
createWindow({ startHidden: false });
}
});
//osX only ends
+47
View File
@@ -47,6 +47,10 @@ function SettingsPage() {
queryFn: getPlatform,
});
const isAutoStartSupported =
platform === 'win32' || platform === 'darwin' || platform === 'linux';
const isMac = platform === 'darwin';
const handleLanguageChange = (value: string) => {
setAppLanguage(value, i18n);
};
@@ -179,6 +183,49 @@ function SettingsPage() {
</CardContent>
</Card>
{isAutoStartSupported && (
<Card>
<CardHeader>
<CardTitle>{t('settings.startup.title', 'Startup')}</CardTitle>
<CardDescription>
{t(
'settings.startup.description',
'Control application launch behavior at system startup.',
)}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.startup.auto_startup', 'Start with system')}</Label>
<p className="text-xs text-gray-500">
{t(
'settings.startup.auto_startup_desc',
'Launch at sign-in and keep the app in the system tray',
)}
</p>
</div>
<Switch
checked={config?.auto_startup || false}
onCheckedChange={async (checked) => {
if (config) {
await saveConfig({ ...config, auto_startup: checked });
}
}}
/>
</div>
{isMac && (
<p className="text-xs text-muted-foreground">
{t(
'settings.startup.macos_hint',
'macOS requires a signed app for Login Items to work. If auto-start fails, please sign the app or enable it manually in System Settings.',
)}
</p>
)}
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>{t('settings.about.title')}</CardTitle>
+2
View File
@@ -22,6 +22,7 @@ export const AppConfigSchema = z.object({
refresh_interval: z.number(), // minutes
auto_sync: z.boolean(), // 是否自动同步
sync_interval: z.number(), // minutes
auto_startup: z.boolean(), // 是否随系统启动
default_export_path: z.string().nullable().optional(), // 导出路径
proxy: ProxyConfigSchema,
});
@@ -37,6 +38,7 @@ export const DEFAULT_APP_CONFIG: AppConfig = {
refresh_interval: 15,
auto_sync: false,
sync_interval: 5,
auto_startup: false,
default_export_path: null,
proxy: {
enabled: false,
+126
View File
@@ -0,0 +1,126 @@
import { app } from 'electron';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { logger } from './logger';
import { AppConfig } from '../types/config';
export const AUTO_START_ARG = '--autostart';
const LINUX_AUTOSTART_FILENAME = 'antigravity-manager.desktop';
function getLinuxAutoStartPath() {
const dir = path.join(os.homedir(), '.config', 'autostart');
return path.join(dir, LINUX_AUTOSTART_FILENAME);
}
export function isAutoStartLaunch(argv = process.argv) {
if (process.platform === 'linux') {
return argv.includes(AUTO_START_ARG);
}
if (process.platform === 'win32' || process.platform === 'darwin') {
try {
const settings = app.getLoginItemSettings();
return settings.wasOpenedAtLogin;
} catch (error) {
logger.error('AutoStart: Failed to read login item settings', error);
}
}
return false;
}
function syncWindowsOrMacAutoStart(enabled: boolean) {
try {
app.setLoginItemSettings({
openAtLogin: enabled,
openAsHidden: enabled,
path: process.execPath,
args: enabled ? [AUTO_START_ARG] : [],
});
const settings = app.getLoginItemSettings();
if (settings.openAtLogin !== enabled) {
logger.warn(
`AutoStart: Login item mismatch (expected ${enabled}, actual ${settings.openAtLogin})`,
);
}
logger.info(`AutoStart: Login item set to ${enabled}`);
} catch (error) {
logger.error('AutoStart: Failed to update login item settings', error);
}
}
function cleanupLinuxAutoStartEntries(currentPath: string) {
const autoStartDir = path.dirname(currentPath);
if (!fs.existsSync(autoStartDir)) {
return;
}
const legacyNames = [
`${app.getName()}.desktop`,
`${app.getName().replace(/\s+/g, '')}.desktop`,
'Antigravity Manager.desktop',
'AntigravityManager.desktop',
];
for (const name of legacyNames) {
const candidate = path.join(autoStartDir, name);
if (candidate === currentPath) {
continue;
}
if (fs.existsSync(candidate)) {
try {
fs.unlinkSync(candidate);
logger.info(`AutoStart: Removed legacy autostart entry ${candidate}`);
} catch (error) {
logger.warn('AutoStart: Failed to remove legacy autostart entry', error);
}
}
}
}
function syncLinuxAutoStart(enabled: boolean) {
const autoStartPath = getLinuxAutoStartPath();
if (!enabled) {
if (fs.existsSync(autoStartPath)) {
fs.unlinkSync(autoStartPath);
logger.info('AutoStart: Removed Linux autostart entry');
}
cleanupLinuxAutoStartEntries(autoStartPath);
return;
}
const autoStartDir = path.dirname(autoStartPath);
if (!fs.existsSync(autoStartDir)) {
fs.mkdirSync(autoStartDir, { recursive: true });
}
const execPath = process.execPath.replace(/"/g, '\\"');
const content = [
'[Desktop Entry]',
'Type=Application',
`Name=${app.getName()}`,
`Exec="${execPath}" ${AUTO_START_ARG}`,
'X-GNOME-Autostart-enabled=true',
'Hidden=false',
'NoDisplay=false',
].join('\n');
cleanupLinuxAutoStartEntries(autoStartPath);
fs.writeFileSync(autoStartPath, content, 'utf-8');
logger.info('AutoStart: Created Linux autostart entry');
}
export function syncAutoStart(config: AppConfig) {
const enabled = Boolean(config.auto_startup);
if (process.platform === 'win32' || process.platform === 'darwin') {
syncWindowsOrMacAutoStart(enabled);
return;
}
if (process.platform === 'linux') {
syncLinuxAutoStart(enabled);
}
}