feat(console): 开机自启默认关闭,服务管理窗口加手动开关

- DEFAULTS.startup.openAtLogin 与随包 config.json 默认改为 false;
  applyOpenAtLogin 只有显式 true 才注册登录项(缺省/null/false 都不注册)
- config.js 新增 updateConfig():只写回文件里已有的键 + 目标键,不把 DEFAULTS 落盘
- 主进程 setOpenAtLogin():改内存 → 写回 config.json → setLoginItemSettings,
  任一失败都回滚,返回 {ok, openAtLogin};状态快照新增 openAtLogin / openAtLoginSupported
- 渲染层:服务端状态卡片新增「开机自启」滑块(纯 CSS,无依赖),
  切换即时生效并有本地意图守卫,避免被随后到达的陈旧快照冲掉
- 文档:console/README.md 与 docs/部署指南.md 同步

注意:旧包 config.json 里已显式写 true 的用户升级后仍会自启(不悄悄改既有行为),
在窗口里关一次即可;全新安装默认不注册。
This commit is contained in:
AskaEth committed 2026-09-10 00:06:59 +08:00
1 parent 7c59dba9b9
commit 1b78c38bb8
9 files changed
+204 -12

No files matched your search

+14 -3
View File
@@ -39,8 +39,8 @@ const DEFAULTS = {
backgroundColor: '#0b0f1a'
},
startup: {
// 打包后随系统启动(doc 10 §4.3);开发态不注册
openAtLogin: true
// 默认不随系统启动;服务管理窗口里有开关,打开后写入登录项
openAtLogin: false
},
admin: {
width: 1040,
@@ -86,6 +86,17 @@ function loadConfig(configPath) {
}
}
// 就地改配置并写回文件:只动 raw 里已有的键,不把 DEFAULTS 全量落盘,
// 否则升级新增的默认值会被老文件冻结成显式值。
function updateConfig(configPath, mutate) {
const { values } = readJson(configPath)
const raw = isPlainObject(values) ? values : {}
mutate(raw)
fs.mkdirSync(path.dirname(configPath), { recursive: true })
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + '\n', 'utf8')
return raw
}
// 相对路径的 exe 需要按「部署包根目录」理解,而部署包根目录在不同形态下不同:
// 开发时是 console/ 的上级,打包后是 exe 所在目录。逐个候选位置探测,避免写死。
function resolveExecutable(exe, configPath) {
@@ -104,4 +115,4 @@ function resolveExecutable(exe, configPath) {
return { path: found || candidates[0], candidates: [...new Set(candidates)] }
}
module.exports = { loadConfig, resolveExecutable }
module.exports = { loadConfig, updateConfig, resolveExecutable }
+51 -3
View File
@@ -11,7 +11,7 @@ const {
powerSaveBlocker
} = require('electron')
const { loadConfig, resolveExecutable } = require('./config')
const { loadConfig, updateConfig, resolveExecutable } = require('./config')
const { LogBus } = require('./log-bus')
const { ServiceSupervisor, healthHost } = require('./service')
const { DisplayManager } = require('./displays')
@@ -64,6 +64,8 @@ function main() {
configWarning: cfg ? cfg.warning : null,
displays: displays ? displays.getInfo() : [],
powerSaveBlocked: powerBlockerId !== null ? powerSaveBlocker.isStarted(powerBlockerId) : false,
openAtLogin: cfg ? cfg.values.startup.openAtLogin === true : false,
openAtLoginSupported: app.isPackaged && (process.platform === 'win32' || process.platform === 'darwin'),
appVersion: app.getVersion()
}
}
@@ -296,7 +298,8 @@ function main() {
// 开机自启:只对打包后的 Windows/macOS 程序有意义,开发态注册的是 electron 可执行文件
function applyOpenAtLogin() {
if (!app.isPackaged || (process.platform !== 'win32' && process.platform !== 'darwin')) return
const wanted = !cfg.values.startup || cfg.values.startup.openAtLogin !== false
// 只有显式 true 才注册;缺省 / null / false 一律不写登录项
const wanted = cfg.values.startup?.openAtLogin === true
try {
app.setLoginItemSettings({ openAtLogin: wanted })
log.push('console', wanted ? '开机自启已启用' : '开机自启已关闭')
@@ -305,6 +308,50 @@ function main() {
}
}
// 配置为准:把开关值落到 config.json(只动 startup.openAtLogin)
function writeOpenAtLogin(value) {
updateConfig(cfg.path, (raw) => {
raw.startup = { ...(raw.startup || {}), openAtLogin: value }
})
}
// 服务管理窗口的「开机自启」开关。Windows 的 Run 项没有「已注册但禁用」的
// 可靠中间态(任务管理器那种禁用走 StartupApproved 私有键,不可靠),
// 所以「不启动」= 不写登录项:打开时写入,关闭时删除。
function setOpenAtLogin(enabled) {
if (!cfg) return { ok: false, message: '配置尚未加载' }
const wanted = enabled === true
const previous = cfg.values.startup?.openAtLogin === true
cfg.values.startup = { ...(cfg.values.startup || {}), openAtLogin: wanted }
try {
writeOpenAtLogin(wanted)
} catch (err) {
cfg.values.startup.openAtLogin = previous
log.push('console', `写入开机自启配置失败:${err.message}`)
return { ok: false, message: `保存配置失败:${err.message}` }
}
if (app.isPackaged && (process.platform === 'win32' || process.platform === 'darwin')) {
try {
app.setLoginItemSettings({ openAtLogin: wanted })
} catch (err) {
// 登录项没写成功,配置也回滚,避免界面与系统状态不一致
cfg.values.startup.openAtLogin = previous
try {
writeOpenAtLogin(previous)
} catch (rollbackErr) {
log.push('console', `回滚开机自启配置失败:${rollbackErr.message}`)
}
log.push('console', `设置开机自启失败:${err.message}`)
return { ok: false, message: `写入登录项失败:${err.message}` }
}
}
log.push('console', wanted ? '开机自启已启用' : '开机自启已关闭')
return { ok: true, openAtLogin: wanted }
}
// 服务端用什么配置文件,passwd 就得用同一份,否则改的是另一个 data/ 下的账号
function passwdConfigArg() {
const args = Array.isArray(cfg.values.server.args) ? cfg.values.server.args : []
@@ -400,6 +447,7 @@ function main() {
return true
})
ipcMain.handle('display:instances-get', () => pollDisplayInstances())
ipcMain.handle('startup:set-open-at-login', (event, enabled) => setOpenAtLogin(enabled))
// 窗口控制统一从 event.sender 反查窗口,多个窗口共用同一套通道
ipcMain.handle('window:minimize', (event) => {
@@ -488,7 +536,7 @@ function main() {
// 避免第二个实例在配置加载完成前触发 showAdmin() 读到 null
app.on('second-instance', () => showAdmin())
// 现场无人值守,Console 需要随系统启动(doc 10 §4.3)
// 开机自启默认关闭,按 config.json 的 startup.openAtLogin 决定是否注册
applyOpenAtLogin()
// 防熄屏要在服务端起来之前就生效,否则开机后第一段时间屏幕仍会休眠