新增开机自启动 [Enhancement]: 希望能够支持静默启动和开机自启动

Fixes #516
This commit is contained in:
xuncha
2026-04-03 21:08:05 +08:00
parent 81b8960d41
commit 758de9949b
6 changed files with 221 additions and 1 deletions

View File

@@ -171,6 +171,118 @@ const AUTO_UPDATE_ENABLED =
process.env.AUTO_UPDATE_ENABLED === '1' || process.env.AUTO_UPDATE_ENABLED === '1' ||
(process.env.AUTO_UPDATE_ENABLED == null && !process.env.VITE_DEV_SERVER_URL) (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<typeof app.setLoginItemSettings>[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++ 运行库劫持。 // 使用白名单过滤 PATH避免被第三方目录中的旧版 VC++ 运行库劫持。
// 仅保留系统目录Windows/System32/SysWOW64和应用自身目录可执行目录、resources // 仅保留系统目录Windows/System32/SysWOW64和应用自身目录可执行目录、resources
function sanitizePathEnv() { function sanitizePathEnv() {
@@ -1250,7 +1362,12 @@ function registerIpcHandlers() {
}) })
ipcMain.handle('config:set', async (_, key: string, value: any) => { 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') { if (key === 'updateChannel') {
applyAutoUpdateChannel('settings') applyAutoUpdateChannel('settings')
} }
@@ -1259,6 +1376,12 @@ function registerIpcHandlers() {
}) })
ipcMain.handle('config:clear', async () => { ipcMain.handle('config:clear', async () => {
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
const result = setSystemLaunchAtStartup(false)
if (!result.success && result.error) {
console.error('[WeFlow] 清空配置时关闭开机自启动失败:', result.error)
}
}
configService?.clear() configService?.clear()
messagePushService.handleConfigCleared() messagePushService.handleConfigCleared()
return true return true
@@ -1301,6 +1424,14 @@ function registerIpcHandlers() {
return app.getVersion() 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 () => { ipcMain.handle('app:checkWayland', async () => {
if (process.platform !== 'linux') return false; if (process.platform !== 'linux') return false;
@@ -2881,6 +3012,7 @@ app.whenReady().then(async () => {
updateSplashProgress(5, '正在加载配置...') updateSplashProgress(5, '正在加载配置...')
configService = new ConfigService() configService = new ConfigService()
applyAutoUpdateChannel('startup') applyAutoUpdateChannel('startup')
syncLaunchAtStartupPreference()
// 将用户主题配置推送给 Splash 窗口 // 将用户主题配置推送给 Splash 窗口
if (splashWindow && !splashWindow.isDestroyed()) { if (splashWindow && !splashWindow.isDestroyed()) {

View File

@@ -53,6 +53,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
app: { app: {
getDownloadsPath: () => ipcRenderer.invoke('app:getDownloadsPath'), getDownloadsPath: () => ipcRenderer.invoke('app:getDownloadsPath'),
getVersion: () => ipcRenderer.invoke('app:getVersion'), getVersion: () => ipcRenderer.invoke('app:getVersion'),
getLaunchAtStartupStatus: () => ipcRenderer.invoke('app:getLaunchAtStartupStatus'),
setLaunchAtStartup: (enabled: boolean) => ipcRenderer.invoke('app:setLaunchAtStartup', enabled),
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'), checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'), downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'),
ignoreUpdate: (version: string) => ipcRenderer.invoke('app:ignoreUpdate', version), ignoreUpdate: (version: string) => ipcRenderer.invoke('app:ignoreUpdate', version),

View File

@@ -27,6 +27,7 @@ interface ConfigSchema {
themeId: string themeId: string
language: string language: string
logEnabled: boolean logEnabled: boolean
launchAtStartup?: boolean
llmModelPath: string llmModelPath: string
whisperModelName: string whisperModelName: string
whisperModelDir: string whisperModelDir: string

View File

@@ -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 [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'>('top-right')
const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all') const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all')
const [notificationFilterList, setNotificationFilterList] = useState<string[]>([]) const [notificationFilterList, setNotificationFilterList] = useState<string[]>([])
const [launchAtStartup, setLaunchAtStartup] = useState(false)
const [launchAtStartupSupported, setLaunchAtStartupSupported] = useState(isWindows || isMac)
const [launchAtStartupReason, setLaunchAtStartupReason] = useState('')
const [windowCloseBehavior, setWindowCloseBehavior] = useState<configService.WindowCloseBehavior>('ask') const [windowCloseBehavior, setWindowCloseBehavior] = useState<configService.WindowCloseBehavior>('ask')
const [quoteLayout, setQuoteLayout] = useState<configService.QuoteLayout>('quote-top') const [quoteLayout, setQuoteLayout] = useState<configService.QuoteLayout>('quote-top')
const [updateChannel, setUpdateChannel] = useState<configService.UpdateChannel>('stable') const [updateChannel, setUpdateChannel] = useState<configService.UpdateChannel>('stable')
@@ -162,6 +165,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const [isFetchingDbKey, setIsFetchingDbKey] = useState(false) const [isFetchingDbKey, setIsFetchingDbKey] = useState(false)
const [isFetchingImageKey, setIsFetchingImageKey] = useState(false) const [isFetchingImageKey, setIsFetchingImageKey] = useState(false)
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false) const [isCheckingUpdate, setIsCheckingUpdate] = useState(false)
const [isUpdatingLaunchAtStartup, setIsUpdatingLaunchAtStartup] = useState(false)
const [appVersion, setAppVersion] = useState('') const [appVersion, setAppVersion] = useState('')
const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null) const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null)
const [showDecryptKey, setShowDecryptKey] = useState(false) const [showDecryptKey, setShowDecryptKey] = useState(false)
@@ -337,6 +341,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const savedNotificationFilterMode = await configService.getNotificationFilterMode() const savedNotificationFilterMode = await configService.getNotificationFilterMode()
const savedNotificationFilterList = await configService.getNotificationFilterList() const savedNotificationFilterList = await configService.getNotificationFilterList()
const savedMessagePushEnabled = await configService.getMessagePushEnabled() const savedMessagePushEnabled = await configService.getMessagePushEnabled()
const savedLaunchAtStartupStatus = await window.electronAPI.app.getLaunchAtStartupStatus()
const savedWindowCloseBehavior = await configService.getWindowCloseBehavior() const savedWindowCloseBehavior = await configService.getWindowCloseBehavior()
const savedQuoteLayout = await configService.getQuoteLayout() const savedQuoteLayout = await configService.getQuoteLayout()
const savedUpdateChannel = await configService.getUpdateChannel() const savedUpdateChannel = await configService.getUpdateChannel()
@@ -386,6 +391,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
setNotificationFilterMode(savedNotificationFilterMode) setNotificationFilterMode(savedNotificationFilterMode)
setNotificationFilterList(savedNotificationFilterList) setNotificationFilterList(savedNotificationFilterList)
setMessagePushEnabled(savedMessagePushEnabled) setMessagePushEnabled(savedMessagePushEnabled)
setLaunchAtStartup(savedLaunchAtStartupStatus.enabled)
setLaunchAtStartupSupported(savedLaunchAtStartupStatus.supported)
setLaunchAtStartupReason(savedLaunchAtStartupStatus.reason || '')
setWindowCloseBehavior(savedWindowCloseBehavior) setWindowCloseBehavior(savedWindowCloseBehavior)
setQuoteLayout(savedQuoteLayout) setQuoteLayout(savedQuoteLayout)
if (savedUpdateChannel) { 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) => { const refreshWhisperStatus = async (modelDirValue = whisperModelDir) => {
try { try {
const result = await window.electronAPI.whisper?.getModelStatus() const result = await window.electronAPI.whisper?.getModelStatus()
@@ -1199,6 +1230,39 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="divider" /> <div className="divider" />
<div className="form-group">
<label></label>
<span className="form-hint">
{launchAtStartupSupported
? '开启后,登录系统时会自动启动 WeFlow。'
: launchAtStartupReason || '当前环境暂不支持开机自启动。'}
</span>
<div className="log-toggle-line">
<span className="log-status">
{isUpdatingLaunchAtStartup
? '保存中...'
: launchAtStartupSupported
? (launchAtStartup ? '已开启' : '已关闭')
: '当前不可用'}
</span>
<label className="switch" htmlFor="launch-at-startup-toggle">
<input
id="launch-at-startup-toggle"
className="switch-input"
type="checkbox"
checked={launchAtStartup}
disabled={!launchAtStartupSupported || isUpdatingLaunchAtStartup}
onChange={(e) => {
void handleLaunchAtStartupChange(e.target.checked)
}}
/>
<span className="switch-slider" />
</label>
</div>
</div>
<div className="divider" />
<div className="form-group"> <div className="form-group">
<label></label> <label></label>
<span className="form-hint"></span> <span className="form-hint"></span>

View File

@@ -13,6 +13,7 @@ export const CONFIG_KEYS = {
LAST_SESSION: 'lastSession', LAST_SESSION: 'lastSession',
WINDOW_BOUNDS: 'windowBounds', WINDOW_BOUNDS: 'windowBounds',
CACHE_PATH: 'cachePath', CACHE_PATH: 'cachePath',
LAUNCH_AT_STARTUP: 'launchAtStartup',
EXPORT_PATH: 'exportPath', EXPORT_PATH: 'exportPath',
AGREEMENT_ACCEPTED: 'agreementAccepted', AGREEMENT_ACCEPTED: 'agreementAccepted',
@@ -258,6 +259,18 @@ export async function setLogEnabled(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.LOG_ENABLED, enabled) await config.set(CONFIG_KEYS.LOG_ENABLED, enabled)
} }
// 获取开机自启动偏好
export async function getLaunchAtStartup(): Promise<boolean | null> {
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<void> {
await config.set(CONFIG_KEYS.LAUNCH_AT_STARTUP, enabled)
}
// 获取 LLM 模型路径 // 获取 LLM 模型路径
export async function getLlmModelPath(): Promise<string | null> { export async function getLlmModelPath(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.LLM_MODEL_PATH) const value = await config.get(CONFIG_KEYS.LLM_MODEL_PATH)

View File

@@ -56,6 +56,14 @@ export interface ElectronAPI {
app: { app: {
getDownloadsPath: () => Promise<string> getDownloadsPath: () => Promise<string>
getVersion: () => Promise<string> getVersion: () => Promise<string>
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 }> checkForUpdates: () => Promise<{ hasUpdate: boolean; version?: string; releaseNotes?: string }>
downloadAndInstall: () => Promise<void> downloadAndInstall: () => Promise<void>
ignoreUpdate: (version: string) => Promise<{ success: boolean }> ignoreUpdate: (version: string) => Promise<{ success: boolean }>