feat(electron): add persistent auth, auto-updater, and NAS copy script
- Integrate electron-store for persistent PocketBase auth backup across localStorage clears and app reinstalls - Switch build target from portable to nsis to generate latest.yml for electron-updater generic provider - Add user confirmation dialogs before download and before install - Add post-build script to copy .exe/.yml/.nupkg to NAS share and local electron-update/ directory for nginx volume mount - Mount ./electron-update into frontend nginx containers via docker-compose for automatic update file serving Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+125
-2
@@ -1,5 +1,22 @@
|
||||
const { app, BrowserWindow, session } = require('electron')
|
||||
const { app, BrowserWindow, session, dialog, ipcMain } = require('electron')
|
||||
const { autoUpdater } = require('electron-updater')
|
||||
const path = require('path')
|
||||
const Store = require('electron-store')
|
||||
|
||||
const store = new Store()
|
||||
|
||||
// 同步 IPC:供 preload 读写持久化数据
|
||||
ipcMain.on('store-get-sync', (event, key) => {
|
||||
event.returnValue = store.get(key)
|
||||
})
|
||||
ipcMain.on('store-set-sync', (event, key, value) => {
|
||||
store.set(key, value)
|
||||
event.returnValue = true
|
||||
})
|
||||
ipcMain.on('store-delete-sync', (event, key) => {
|
||||
store.delete(key)
|
||||
event.returnValue = true
|
||||
})
|
||||
|
||||
const ENV_URLS = {
|
||||
dev: 'http://192.168.1.14:7033',
|
||||
@@ -15,11 +32,19 @@ function getWindowUrl() {
|
||||
return ENV_URLS.dev
|
||||
}
|
||||
|
||||
function getEnvName() {
|
||||
const url = getWindowUrl()
|
||||
if (url.includes('7034')) return 'uat'
|
||||
return 'dev'
|
||||
}
|
||||
|
||||
// 在 Chromium 启动前将 HTTP 内网地址标记为安全源,
|
||||
// 否则 navigator.mediaDevices 在 HTTP 非 localhost 下会被置空
|
||||
const insecureOrigins = Object.values(ENV_URLS).join(',')
|
||||
app.commandLine.appendSwitch('unsafely-treat-insecure-origin-as-secure', insecureOrigins)
|
||||
|
||||
let mainWindow = null
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 1280,
|
||||
@@ -36,6 +61,8 @@ function createWindow() {
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow = win
|
||||
|
||||
const url = getWindowUrl()
|
||||
win.loadURL(url)
|
||||
|
||||
@@ -44,6 +71,101 @@ function createWindow() {
|
||||
event.preventDefault()
|
||||
win.setTitle(`Game Group - ${title}`)
|
||||
})
|
||||
|
||||
win.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
// 自动更新配置
|
||||
function setupAutoUpdater(win) {
|
||||
const env = getEnvName()
|
||||
const feedUrl = `${ENV_URLS[env]}/electron-update/`
|
||||
|
||||
autoUpdater.setFeedURL({ provider: 'generic', url: feedUrl })
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = false
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
console.log('[updater] Checking for update at', feedUrl)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
console.log('[updater] Update available:', info.version)
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('update-available', info)
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'info',
|
||||
title: '发现新版本',
|
||||
message: `发现新版本 ${info.version},是否立即下载更新?`,
|
||||
buttons: ['立即下载', '稍后再说'],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
}).then(({ response }) => {
|
||||
if (response === 0) {
|
||||
autoUpdater.downloadUpdate().catch((err) => {
|
||||
console.error('[updater] Download failed:', err.message)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
console.log('[updater] No update available')
|
||||
})
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
console.error('[updater] Error:', err.message)
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('update-error', err.message)
|
||||
}
|
||||
})
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
console.log(`[updater] Download progress: ${progress.percent.toFixed(1)}%`)
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('download-progress', progress)
|
||||
}
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
console.log('[updater] Update downloaded:', info.version)
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('update-downloaded', info)
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'info',
|
||||
title: '更新就绪',
|
||||
message: `新版本 ${info.version} 已下载完成,是否立即重启安装?`,
|
||||
buttons: ['立即重启', '稍后'],
|
||||
defaultId: 0,
|
||||
}).then(({ response }) => {
|
||||
if (response === 0) {
|
||||
autoUpdater.quitAndInstall()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 手动触发检查更新(供前端调用)
|
||||
ipcMain.on('check-for-updates', () => {
|
||||
autoUpdater.checkForUpdates().catch((err) => {
|
||||
console.error('[updater] Manual check failed:', err.message)
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('quit-and-install', () => {
|
||||
autoUpdater.quitAndInstall()
|
||||
})
|
||||
|
||||
// 启动后延迟 5 秒检查更新(等窗口稳定后再检查)
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates().catch((err) => {
|
||||
console.error('[updater] Initial check failed:', err.message)
|
||||
})
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
@@ -64,7 +186,8 @@ app.whenReady().then(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
createWindow()
|
||||
const win = createWindow()
|
||||
setupAutoUpdater(win)
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
|
||||
+20
-4
@@ -7,11 +7,15 @@
|
||||
"start": "electron .",
|
||||
"start:dev": "electron . --env=dev",
|
||||
"start:uat": "electron . --env=uat",
|
||||
"build": "electron-builder --win",
|
||||
"build:portable": "electron-builder --win portable"
|
||||
"build": "electron-builder --win && node scripts/copy-to-nas.js",
|
||||
"build:portable": "electron-builder --win portable && node scripts/copy-to-nas.js",
|
||||
"copy-to-nas": "node scripts/copy-to-nas.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-store": "^8.2"
|
||||
"electron-store": "^8.2",
|
||||
"electron-updater": "^6.8.3",
|
||||
"png-to-ico": "^3.0.1",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^35.0",
|
||||
@@ -26,12 +30,24 @@
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "portable",
|
||||
"target": "nsis",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"icon": "build/icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "GameGroup"
|
||||
},
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": "http://nas.wjl-work.top:7034/electron-update/",
|
||||
"channel": "latest"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"preload.js",
|
||||
|
||||
+15
-2
@@ -1,6 +1,19 @@
|
||||
// preload.js - 目前为空,预留用于未来需要暴露给渲染进程的 API
|
||||
const { contextBridge } = require('electron')
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
platform: process.platform,
|
||||
|
||||
// 自动更新相关 IPC
|
||||
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', (_event, info) => callback(info)),
|
||||
onUpdateError: (callback) => ipcRenderer.on('update-error', (_event, message) => callback(message)),
|
||||
onDownloadProgress: (callback) => ipcRenderer.on('download-progress', (_event, progress) => callback(progress)),
|
||||
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', (_event, info) => callback(info)),
|
||||
|
||||
checkForUpdates: () => ipcRenderer.send('check-for-updates'),
|
||||
quitAndInstall: () => ipcRenderer.send('quit-and-install'),
|
||||
|
||||
// 持久化存储(用于记住登录状态)
|
||||
storeGet: (key) => ipcRenderer.sendSync('store-get-sync', key),
|
||||
storeSet: (key, value) => ipcRenderer.sendSync('store-set-sync', key, value),
|
||||
storeDelete: (key) => ipcRenderer.sendSync('store-delete-sync', key),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const srcDir = path.join(__dirname, '..', 'dist')
|
||||
// 项目根目录下的 electron-update,用于 docker volume 挂载给 nginx
|
||||
const localUpdateDir = path.join(__dirname, '..', '..', 'electron-update')
|
||||
// NAS 共享路径
|
||||
const nasDir = '\\\\JIULUGNAS\\personal_folder\\CodeSpace\\GameGroup2'
|
||||
|
||||
function ensureDir(dir) {
|
||||
try {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(`[copy-to-nas] failed to create dir ${dir}:`, err.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function copyFilesTo(destDir, label) {
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
console.log(`[copy-to-nas] dist not found, skipping ${label}`)
|
||||
return
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(srcDir).filter((f) => {
|
||||
// 只复制更新相关文件:安装包、yml、blockmap、nupkg
|
||||
const ext = path.extname(f).toLowerCase()
|
||||
return (
|
||||
ext === '.exe' ||
|
||||
ext === '.yml' ||
|
||||
ext === '.blockmap' ||
|
||||
ext === '.nupkg' ||
|
||||
ext === '.yaml'
|
||||
)
|
||||
})
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log(`[copy-to-nas] no update files found in dist, skipping ${label}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!ensureDir(destDir)) return
|
||||
|
||||
for (const file of files) {
|
||||
const src = path.join(srcDir, file)
|
||||
const dest = path.join(destDir, file)
|
||||
try {
|
||||
fs.copyFileSync(src, dest)
|
||||
console.log(`[copy-to-nas] copied ${file} -> ${destDir}`)
|
||||
} catch (err) {
|
||||
console.error(`[copy-to-nas] failed to copy ${file} to ${label}:`, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 复制到本地目录(供 docker volume 挂载)
|
||||
copyFilesTo(localUpdateDir, 'local')
|
||||
|
||||
// 复制到 NAS
|
||||
copyFilesTo(nasDir, 'NAS')
|
||||
Reference in New Issue
Block a user