初始化 SSH 面板:Electron + React + electron-vite
- 登录页:服务器地址/端口/用户名/密码表单,ssh2 建立真实 SSH 连接 - 面板页:深蓝色顶栏菜单(概览/容器/终端占位),显示当前连接 - 凭据保存:密码经系统安全存储(DPAPI)加密后保存在本机,支持一键填充/删除 - 隐藏 Electron 默认菜单,开发模式 F12 开关开发者工具 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import { app, BrowserWindow, Menu, ipcMain, safeStorage } from 'electron'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { Client } from 'ssh2'
|
||||
|
||||
// 隐藏 Electron 默认菜单(File / Edit 等)
|
||||
Menu.setApplicationMenu(null)
|
||||
|
||||
let mainWindow = null
|
||||
let activeConnection = null // 当前活动的 SSH 连接
|
||||
let activeSession = null // { host, username },登录成功后记录
|
||||
|
||||
// ---------- 凭据存储 ----------
|
||||
const credFile = () => path.join(app.getPath('userData'), 'credentials.json')
|
||||
|
||||
function readCredentials() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(credFile(), 'utf8'))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function writeCredentials(list) {
|
||||
fs.mkdirSync(path.dirname(credFile()), { recursive: true })
|
||||
fs.writeFileSync(credFile(), JSON.stringify(list, null, 2), 'utf8')
|
||||
}
|
||||
|
||||
// 密码优先用系统安全存储加密(Windows DPAPI),不可用时降级为 base64
|
||||
function encryptPassword(password) {
|
||||
if (safeStorage.isEncryptionAvailable()) {
|
||||
return { data: safeStorage.encryptString(password).toString('base64'), encrypted: true }
|
||||
}
|
||||
return { data: Buffer.from(password, 'utf8').toString('base64'), encrypted: false }
|
||||
}
|
||||
|
||||
function decryptPassword(entry) {
|
||||
if (entry.encrypted) {
|
||||
return safeStorage.decryptString(Buffer.from(entry.data, 'base64'))
|
||||
}
|
||||
return Buffer.from(entry.data, 'base64').toString('utf8')
|
||||
}
|
||||
|
||||
// ---------- 窗口 ----------
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
backgroundColor: '#d2f8fc',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
})
|
||||
|
||||
if (process.env['ELECTRON_RENDERER_URL']) {
|
||||
// 开发模式:加载 Vite 开发服务器(支持热更新)
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
// 菜单已隐藏,用 F12 开关开发者工具
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
if (input.key === 'F12') mainWindow.webContents.toggleDevTools()
|
||||
})
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
if (activeConnection) {
|
||||
activeConnection.end()
|
||||
activeConnection = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- IPC:SSH 连接 ----------
|
||||
ipcMain.handle('ssh:connect', (event, { host, port, username, password }) => {
|
||||
return new Promise((resolve) => {
|
||||
// 已有连接时先断开旧连接
|
||||
if (activeConnection) {
|
||||
activeConnection.end()
|
||||
activeConnection = null
|
||||
}
|
||||
|
||||
const client = new Client()
|
||||
|
||||
client.on('ready', () => {
|
||||
activeConnection = client
|
||||
activeSession = { host, username }
|
||||
resolve({ ok: true })
|
||||
})
|
||||
|
||||
client.on('error', (err) => {
|
||||
resolve({ ok: false, message: err.message })
|
||||
})
|
||||
|
||||
client.on('close', () => {
|
||||
if (activeConnection === client) activeConnection = null
|
||||
})
|
||||
|
||||
client.connect({
|
||||
host,
|
||||
port: Number(port),
|
||||
username,
|
||||
password,
|
||||
readyTimeout: 10000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('ssh:disconnect', () => {
|
||||
if (activeConnection) {
|
||||
activeConnection.end()
|
||||
activeConnection = null
|
||||
}
|
||||
activeSession = null
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
ipcMain.handle('ssh:session', () => activeSession)
|
||||
|
||||
// ---------- IPC:凭据 ----------
|
||||
ipcMain.handle('cred:save', (event, { host, port, username, password }) => {
|
||||
const list = readCredentials()
|
||||
const entry = { host, port: Number(port), username, password: encryptPassword(password) }
|
||||
// 同一 host + username 覆盖旧记录
|
||||
const idx = list.findIndex((c) => c.host === host && c.username === username)
|
||||
if (idx >= 0) list[idx] = entry
|
||||
else list.push(entry)
|
||||
writeCredentials(list)
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
ipcMain.handle('cred:list', () => {
|
||||
return readCredentials()
|
||||
.map((c) => {
|
||||
try {
|
||||
return {
|
||||
host: c.host,
|
||||
port: c.port,
|
||||
username: c.username,
|
||||
password: decryptPassword(c.password),
|
||||
}
|
||||
} catch {
|
||||
return null // 本机无法解密(比如从别的机器拷贝来的),跳过
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
ipcMain.handle('cred:delete', (event, { host, username }) => {
|
||||
writeCredentials(readCredentials().filter((c) => !(c.host === host && c.username === username)))
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
// ---------- 应用生命周期 ----------
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
Reference in New Issue
Block a user