PicGo 开机自启的隐藏 Bug
PicGo 开机自启的隐藏 Bug
从GitHub下载的最新版本(3.0.2),却遇到"开启自启后重启多一个启动项",拉取源码修改后出现"静默启动不生效"的问题。本文记录完整的排查与修复过程,涉及 Electron 的三个隐藏坑点。
问题现象
PicGo 是基于 Electron + Vue 3 的图床客户端,Windows 下设置页有"开机自启"开关。从 GitHub 拉取最新代码、构建安装后出现两个问题:
- 启动项重复:在设置中开启开机自启,每次运行应用都会在注册表里多出一个启动项。原本只有
com.molunerfinn.picgo,运行后却多出electron.app.PicGo。 - 静默启动失效:即使把"启动模式"设为"隐藏",重启计算机后主窗口仍然弹出。
排查思路
两个症状看似独立,实则牵涉三层不同问题。排查流程如下:
症状1(启动项重复) → 检查 setLoginItemSettings 调用顺序 → 发现 AUMID 设置时机错误
症状2(静默失效) → 确认 startupMode 配置值 → fallback 默认值不一致
→ 搜索所有 .show() 调用 → 定位 window.maximize() 强制显示
下面逐一展开。
根因一:AUMID 与 setLoginItemSettings 的调用顺序
现象
注册表 HKCU\Software\Microsoft\Windows\CurrentVersion\Run 下出现两个键:
| 键名 | 来源 |
|---|---|
com.molunerfinn.picgo |
用户在设置页开启自启时创建(AUMID 已正确设置) |
electron.app.PicGo |
应用每次启动时再次创建(AUMID 尚未设置,用了默认值) |
根因
Windows 下,Electron 的 app.setLoginItemSettings() 创建的启动项键名由当前的 AppUserModelId(AUMID)派生。如果调用 setLoginItemSettings 时 AUMID 还没设置,Electron 会用默认值 electron.app.<AppName>,导致键名不一致。
原代码位于 src/main/lifeCycle/index.ts 的 onRunning():
// ❌ 修改前:顺序错误
app.setLoginItemSettings({ // 先设置启动项 → AUMID 还是默认值
openAtLogin: picgo.getConfig('settings.autoStart') || false
})
if (process.platform === 'win32') {
app.setAppUserModelId('com.molunerfinn.picgo') // 后设置 AUMID → 为时已晚
}
两次创建的键名不同,所以不会互相覆盖,最终累积成两个启动项。
修复
对调顺序:先设置 AUMID,再调用 setLoginItemSettings,保证键名始终一致。
// ✅ 修改后
if (process.platform === 'win32') {
app.setAppUserModelId('com.molunerfinn.picgo') // 先设 AUMID
cleanStaleWindowsStartupEntry() // 顺便清理历史残留项
}
app.setLoginItemSettings({ // 再设启动项,键名稳定
openAtLogin: picgo.getConfig('settings.autoStart') || false
})
同时新增 cleanStaleWindowsStartupEntry() 一次性清理已累积的 electron.app.PicGo 残留键:
function cleanStaleWindowsStartupEntry (): void {
if (process.platform !== 'win32') return
const runKey = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
const approvedKey = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run'
for (const key of [runKey, approvedKey]) {
try {
execSync(`reg delete "${key}" /v "electron.app.PicGo" /f`, { stdio: 'ignore' })
} catch {
// 键或值不存在时 reg delete 返回非零退出码,属正常情况,忽略即可
}
}
}
要点:Windows 启动项相关操作必须遵循"AUMID 先于一切"的原则,否则任何使用默认 AUMID 的调用都会创建出键名不一致的重复项。
根因二:startupMode 的 fallback 在主进程和渲染进程不一致
现象
修复完根因一后,启动项不再重复,但静默启动依旧失效。检查用户配置文件 %APPDATA%\PicGo\data.json:
{
"settings": {
"autoStart": true,
"startupMode": "HIDE"
}
}
startupMode 已经是 HIDE,说明问题不在配置本身。但在排查配置加载链路时,发现了一个潜在的不一致:全新安装时配置文件没有 startupMode 字段,此时主进程的 fallback 与渲染进程的默认值不一致。
根因
| 层 | 默认 startupMode |
|---|---|
| 渲染进程(设置页显示) | HIDE |
| 主进程 fallback(Windows) | SHOW_MAIN_WINDOW ← 不一致 |
原代码位于 src/main/apis/app/window/windowList.ts:
// ❌ 修改前:Windows fallback 到 SHOW_MAIN_WINDOW
const startupMode = picgo.getConfig<IStartupMode | undefined>('settings.startupMode')
|| (isLinux ? IStartupMode.SHOW_MINI_WINDOW
: isWindows ? IStartupMode.SHOW_MAIN_WINDOW // ← 与渲染进程不一致
: IStartupMode.HIDE)
渲染进程的 defaultSettingsConfig 在 src/renderer/components/main/settings/utils.ts:
startupMode: IStartupMode.HIDE // 渲染进程默认是 HIDE
全新安装时,配置文件中没有 startupMode,用户在设置页看到的是"隐藏"(来自渲染进程默认值),但主进程实际用的是"显示主窗口"(来自 fallback),行为和界面不符。
修复
统一 fallback:Windows 和 macOS 都 fallback 到 HIDE,Linux 保持 SHOW_MINI_WINDOW:
// ✅ 修改后
const startupMode = picgo.getConfig<IStartupMode | undefined>('settings.startupMode')
|| (isLinux ? IStartupMode.SHOW_MINI_WINDOW
: IStartupMode.HIDE) // ← 与渲染进程默认值一致
要点:跨进程的默认值必须保持一致,否则用户看到的界面状态和实际行为会脱节。这种问题在全新安装场景下才会暴露,已有配置的用户不受影响。
根因三:window.maximize() 会强制显示隐藏的窗口
现象
修复完根因二后,理论上静默启动应该生效(配置中 startupMode 已经是 HIDE),但主窗口仍然弹出。这说明有其他地方在调用 window.show() 或类似 API。
排查
全文搜索 src/main 下所有 .show() 调用,最终定位到 SETTING_WINDOW 的 callback:
// windowList.ts 中 SETTING_WINDOW 的 callback
if (getMainWindowState().isMaximized) {
window.maximize() // ← 罪魁祸首
}
检查用户的 window-state.json:
{"mainWindow":{"width":854,"height":513,"isMaximized":true}}
isMaximized: true,所以每次创建 SETTING_WINDOW 时都会调用 window.maximize()。
根因
Electron 官方文档对 win.maximize() 的描述:
Maximizes the window. This will also show (un-hide) the window if it currently isn't.
即 maximize() 会自动 show 隐藏的窗口。这是最隐蔽的一坑:
- SETTING_WINDOW 创建时
show: false(隐藏) isWindowShouldShowOnStartup返回false(因为startupMode === HIDE)→ 不调用settingWindow.show()- 但 callback 里调用了
window.maximize()→ 强制 show 窗口 → 静默启动失败
修复
延迟 maximize 时机:把同步调用改成监听 show 事件后执行。这样只有用户主动打开窗口时才会触发 maximize,静默启动时窗口不 show,once('show') 不会触发,自然不会 maximize。
// ❌ 修改前
if (getMainWindowState().isMaximized) {
window.maximize() // 创建时立即 maximize → 强制显示
}
// ✅ 修改后
window.once('show', () => {
if (getMainWindowState().isMaximized) {
window.maximize() // 窗口 show 时才 maximize
}
})
行为对照:
| 场景 | 窗口是否 show | once('show') 是否触发 |
是否 maximize | 结果 |
|---|---|---|---|---|
静默启动(HIDE) |
否 | 否 | 否 | 窗口不可见 ✓ |
| 正常启动 / 用户手动打开 | 是 | 是 | 是 | 恢复最大化 ✓ |
要点:Electron 的
maximize()、unmaximize()、focus()、restore()等 API 都可能触发show副作用。在需要"创建即隐藏"的场景下,这些调用必须延后到show事件之后。
修复总览
| 根因 | 文件 | 修改点 |
|---|---|---|
| AUMID 设置时机晚于 setLoginItemSettings | src/main/lifeCycle/index.ts |
对调调用顺序 + 新增清理函数 |
| startupMode fallback 主进程/渲染进程不一致 | src/main/apis/app/window/windowList.ts |
Windows fallback 改为 HIDE |
window.maximize() 强制显示隐藏窗口 |
src/main/apis/app/window/windowList.ts |
延迟到 once('show') 后执行 |
三个 Bug 相互独立,但症状交织在一起,排查时容易被前一个修复的"无效"误导。正确的做法是:每修一个问题就重新验证一次,不要假设一次性解决所有症状。
测试技巧
不重启验证开机自启
每次修改都要重启计算机验证太低效。可以用以下方法模拟开机自启:
# 1. 查看注册表里 PicGo 启动项的命令
reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "com.molunerfinn.picgo"
# 2. 彻底退出当前 PicGo(托盘右键 → 退出)
# 3. 用拿到的命令启动应用(通常类似)
& "C:\Users\<用户名>\AppData\Local\Programs\picgo\PicGo.exe"
这样等同于系统开机时拉起 PicGo 的场景,无需重启。
验证启动项注册/清理
# 查看所有 PicGo 相关启动项
reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" | findstr /i picgo
# 手动清理残留项(可选)
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "electron.app.PicGo" /f
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "electron.app.PicGo" /f
验证窗口显示逻辑
开发期可以用 pnpm dev 快速验证 startupMode、maximize 等逻辑(虽然 dev 模式不会真正注册启动项,但窗口显示逻辑是一致的)。
总结
这次修复过程暴露了 Electron 桌面应用的三个典型陷阱:
- AUMID 是 Windows 身份的根:所有依赖 AUMID 派生键名的操作(启动项、通知、快捷方式)都必须在 AUMID 设置之后调用。
- 跨进程默认值必须一致:主进程的 fallback 和渲染进程的默认值要对齐,否则全新安装场景下会出现"界面显示 A,实际行为 B"的诡异问题。
- Electron API 的副作用:
maximize()等 API 会隐式 show 窗口,在需要隐藏创建的场景下必须延后调用。
这些坑在文档里都有说明,但分散在不同页面,且不会直接报错——只在特定场景下才暴露症状。排查的关键是:不要相信"修了一个就该好了"的直觉,每次修改后重新验证,逐个排除。