diff --git a/electron/main.ts b/electron/main.ts index 352651d..b2a93e5 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -171,6 +171,118 @@ const AUTO_UPDATE_ENABLED = process.env.AUTO_UPDATE_ENABLED === '1' || (process.env.AUTO_UPDATE_ENABLED == null && !process.env.VITE_DEV_SERVER_URL) +const getLaunchAtStartupUnsupportedReason = (): string | null => { + if (process.platform !== 'win32' && process.platform !== 'darwin') { + return '当前平台暂不支持开机自启动' + } + if (!app.isPackaged) { + return '仅安装后的 Windows / macOS 版本支持开机自启动' + } + return null +} + +const isLaunchAtStartupSupported = (): boolean => getLaunchAtStartupUnsupportedReason() == null + +const getStoredLaunchAtStartupPreference = (): boolean | undefined => { + const value = configService?.get('launchAtStartup') + return typeof value === 'boolean' ? value : undefined +} + +const getSystemLaunchAtStartup = (): boolean => { + if (!isLaunchAtStartupSupported()) return false + try { + return app.getLoginItemSettings().openAtLogin === true + } catch (error) { + console.error('[WeFlow] 读取开机自启动状态失败:', error) + return false + } +} + +const buildLaunchAtStartupSettings = (enabled: boolean): Parameters[0] => + process.platform === 'win32' + ? { openAtLogin: enabled, path: process.execPath } + : { openAtLogin: enabled } + +const setSystemLaunchAtStartup = (enabled: boolean): { success: boolean; enabled: boolean; error?: string } => { + try { + app.setLoginItemSettings(buildLaunchAtStartupSettings(enabled)) + const effectiveEnabled = app.getLoginItemSettings().openAtLogin === true + if (effectiveEnabled !== enabled) { + return { + success: false, + enabled: effectiveEnabled, + error: '系统未接受该开机自启动设置' + } + } + return { success: true, enabled: effectiveEnabled } + } catch (error) { + return { + success: false, + enabled: getSystemLaunchAtStartup(), + error: `设置开机自启动失败: ${String((error as Error)?.message || error)}` + } + } +} + +const getLaunchAtStartupStatus = (): { enabled: boolean; supported: boolean; reason?: string } => { + const unsupportedReason = getLaunchAtStartupUnsupportedReason() + if (unsupportedReason) { + return { + enabled: getStoredLaunchAtStartupPreference() === true, + supported: false, + reason: unsupportedReason + } + } + return { + enabled: getSystemLaunchAtStartup(), + supported: true + } +} + +const applyLaunchAtStartupPreference = ( + enabled: boolean +): { success: boolean; enabled: boolean; supported: boolean; reason?: string; error?: string } => { + const unsupportedReason = getLaunchAtStartupUnsupportedReason() + if (unsupportedReason) { + return { + success: false, + enabled: getStoredLaunchAtStartupPreference() === true, + supported: false, + reason: unsupportedReason + } + } + + const result = setSystemLaunchAtStartup(enabled) + configService?.set('launchAtStartup', result.enabled) + return { + ...result, + supported: true + } +} + +const syncLaunchAtStartupPreference = () => { + if (!configService) return + + const unsupportedReason = getLaunchAtStartupUnsupportedReason() + if (unsupportedReason) return + + const storedPreference = getStoredLaunchAtStartupPreference() + const systemEnabled = getSystemLaunchAtStartup() + + if (typeof storedPreference !== 'boolean') { + configService.set('launchAtStartup', systemEnabled) + return + } + + if (storedPreference === systemEnabled) return + + const result = setSystemLaunchAtStartup(storedPreference) + configService.set('launchAtStartup', result.enabled) + if (!result.success && result.error) { + console.error('[WeFlow] 同步开机自启动设置失败:', result.error) + } +} + // 使用白名单过滤 PATH,避免被第三方目录中的旧版 VC++ 运行库劫持。 // 仅保留系统目录(Windows/System32/SysWOW64)和应用自身目录(可执行目录、resources)。 function sanitizePathEnv() { @@ -1250,7 +1362,12 @@ function registerIpcHandlers() { }) ipcMain.handle('config:set', async (_, key: string, value: any) => { - const result = configService?.set(key as any, value) + let result: unknown + if (key === 'launchAtStartup') { + result = applyLaunchAtStartupPreference(value === true) + } else { + result = configService?.set(key as any, value) + } if (key === 'updateChannel') { applyAutoUpdateChannel('settings') } @@ -1259,6 +1376,12 @@ function registerIpcHandlers() { }) ipcMain.handle('config:clear', async () => { + if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) { + const result = setSystemLaunchAtStartup(false) + if (!result.success && result.error) { + console.error('[WeFlow] 清空配置时关闭开机自启动失败:', result.error) + } + } configService?.clear() messagePushService.handleConfigCleared() return true @@ -1301,6 +1424,14 @@ function registerIpcHandlers() { return app.getVersion() }) + ipcMain.handle('app:getLaunchAtStartupStatus', async () => { + return getLaunchAtStartupStatus() + }) + + ipcMain.handle('app:setLaunchAtStartup', async (_, enabled: boolean) => { + return applyLaunchAtStartupPreference(enabled === true) + }) + ipcMain.handle('app:checkWayland', async () => { if (process.platform !== 'linux') return false; @@ -2881,6 +3012,7 @@ app.whenReady().then(async () => { updateSplashProgress(5, '正在加载配置...') configService = new ConfigService() applyAutoUpdateChannel('startup') + syncLaunchAtStartupPreference() // 将用户主题配置推送给 Splash 窗口 if (splashWindow && !splashWindow.isDestroyed()) { diff --git a/electron/preload.ts b/electron/preload.ts index 38e722f..db103ef 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -53,6 +53,8 @@ contextBridge.exposeInMainWorld('electronAPI', { app: { getDownloadsPath: () => ipcRenderer.invoke('app:getDownloadsPath'), getVersion: () => ipcRenderer.invoke('app:getVersion'), + getLaunchAtStartupStatus: () => ipcRenderer.invoke('app:getLaunchAtStartupStatus'), + setLaunchAtStartup: (enabled: boolean) => ipcRenderer.invoke('app:setLaunchAtStartup', enabled), checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'), downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'), ignoreUpdate: (version: string) => ipcRenderer.invoke('app:ignoreUpdate', version), diff --git a/electron/services/config.ts b/electron/services/config.ts index 7e3b1e1..3039412 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -27,6 +27,7 @@ interface ConfigSchema { themeId: string language: string logEnabled: boolean + launchAtStartup?: boolean llmModelPath: string whisperModelName: string whisperModelDir: string diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 98fe8b3..808a601 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -138,6 +138,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'>('top-right') const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all') const [notificationFilterList, setNotificationFilterList] = useState([]) + const [launchAtStartup, setLaunchAtStartup] = useState(false) + const [launchAtStartupSupported, setLaunchAtStartupSupported] = useState(isWindows || isMac) + const [launchAtStartupReason, setLaunchAtStartupReason] = useState('') const [windowCloseBehavior, setWindowCloseBehavior] = useState('ask') const [quoteLayout, setQuoteLayout] = useState('quote-top') const [updateChannel, setUpdateChannel] = useState('stable') @@ -162,6 +165,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [isFetchingDbKey, setIsFetchingDbKey] = useState(false) const [isFetchingImageKey, setIsFetchingImageKey] = useState(false) const [isCheckingUpdate, setIsCheckingUpdate] = useState(false) + const [isUpdatingLaunchAtStartup, setIsUpdatingLaunchAtStartup] = useState(false) const [appVersion, setAppVersion] = useState('') const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null) const [showDecryptKey, setShowDecryptKey] = useState(false) @@ -337,6 +341,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedNotificationFilterMode = await configService.getNotificationFilterMode() const savedNotificationFilterList = await configService.getNotificationFilterList() const savedMessagePushEnabled = await configService.getMessagePushEnabled() + const savedLaunchAtStartupStatus = await window.electronAPI.app.getLaunchAtStartupStatus() const savedWindowCloseBehavior = await configService.getWindowCloseBehavior() const savedQuoteLayout = await configService.getQuoteLayout() const savedUpdateChannel = await configService.getUpdateChannel() @@ -386,6 +391,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setNotificationFilterMode(savedNotificationFilterMode) setNotificationFilterList(savedNotificationFilterList) setMessagePushEnabled(savedMessagePushEnabled) + setLaunchAtStartup(savedLaunchAtStartupStatus.enabled) + setLaunchAtStartupSupported(savedLaunchAtStartupStatus.supported) + setLaunchAtStartupReason(savedLaunchAtStartupStatus.reason || '') setWindowCloseBehavior(savedWindowCloseBehavior) setQuoteLayout(savedQuoteLayout) if (savedUpdateChannel) { @@ -428,6 +436,29 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { + const handleLaunchAtStartupChange = async (enabled: boolean) => { + if (isUpdatingLaunchAtStartup) return + + try { + setIsUpdatingLaunchAtStartup(true) + const result = await window.electronAPI.app.setLaunchAtStartup(enabled) + setLaunchAtStartup(result.enabled) + setLaunchAtStartupSupported(result.supported) + setLaunchAtStartupReason(result.reason || '') + + if (result.success) { + showMessage(enabled ? '已开启开机自启动' : '已关闭开机自启动', true) + return + } + + showMessage(result.error || result.reason || '设置开机自启动失败', false) + } catch (e: any) { + showMessage(`设置开机自启动失败: ${e?.message || String(e)}`, false) + } finally { + setIsUpdatingLaunchAtStartup(false) + } + } + const refreshWhisperStatus = async (modelDirValue = whisperModelDir) => { try { const result = await window.electronAPI.whisper?.getModelStatus() @@ -1199,6 +1230,39 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
+
+ + + {launchAtStartupSupported + ? '开启后,登录系统时会自动启动 WeFlow。' + : launchAtStartupReason || '当前环境暂不支持开机自启动。'} + +
+ + {isUpdatingLaunchAtStartup + ? '保存中...' + : launchAtStartupSupported + ? (launchAtStartup ? '已开启' : '已关闭') + : '当前不可用'} + + +
+
+ +
+
设置点击关闭按钮后的默认行为;选择“每次询问”时会弹出关闭确认。 diff --git a/src/services/config.ts b/src/services/config.ts index 59e8afa..1f687e7 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -13,6 +13,7 @@ export const CONFIG_KEYS = { LAST_SESSION: 'lastSession', WINDOW_BOUNDS: 'windowBounds', CACHE_PATH: 'cachePath', + LAUNCH_AT_STARTUP: 'launchAtStartup', EXPORT_PATH: 'exportPath', AGREEMENT_ACCEPTED: 'agreementAccepted', @@ -258,6 +259,18 @@ export async function setLogEnabled(enabled: boolean): Promise { await config.set(CONFIG_KEYS.LOG_ENABLED, enabled) } +// 获取开机自启动偏好 +export async function getLaunchAtStartup(): Promise { + const value = await config.get(CONFIG_KEYS.LAUNCH_AT_STARTUP) + if (typeof value === 'boolean') return value + return null +} + +// 设置开机自启动偏好 +export async function setLaunchAtStartup(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.LAUNCH_AT_STARTUP, enabled) +} + // 获取 LLM 模型路径 export async function getLlmModelPath(): Promise { const value = await config.get(CONFIG_KEYS.LLM_MODEL_PATH) diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index c174983..19f33a5 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -56,6 +56,14 @@ export interface ElectronAPI { app: { getDownloadsPath: () => Promise getVersion: () => Promise + getLaunchAtStartupStatus: () => Promise<{ enabled: boolean; supported: boolean; reason?: string }> + setLaunchAtStartup: (enabled: boolean) => Promise<{ + success: boolean + enabled: boolean + supported: boolean + reason?: string + error?: string + }> checkForUpdates: () => Promise<{ hasUpdate: boolean; version?: string; releaseNotes?: string }> downloadAndInstall: () => Promise ignoreUpdate: (version: string) => Promise<{ success: boolean }>