first add
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const { Client } = require('ssh2');
|
||||
|
||||
let mainWindow;
|
||||
let sshConnections = {};
|
||||
let sftpConnections = {};
|
||||
let shellStreams = {};
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
height: 900,
|
||||
minWidth: 1200,
|
||||
minHeight: 700,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
enableRemoteModule: true
|
||||
},
|
||||
title: 'SSH Client Pro'
|
||||
});
|
||||
|
||||
// 加载页面
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.loadURL('http://localhost:5173');
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../public/index.html'));
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
// 关闭所有SSH连接
|
||||
Object.values(sshConnections).forEach(conn => {
|
||||
try { conn.end(); } catch (e) {}
|
||||
});
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// ========== SSH 连接管理 ==========
|
||||
|
||||
// SSH连接
|
||||
ipcMain.on('ssh-connect', (event, { id, host, port, username, password, privateKey }) => {
|
||||
const conn = new Client();
|
||||
|
||||
conn.on('ready', () => {
|
||||
sshConnections[id] = conn;
|
||||
|
||||
// 通知前端连接成功
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('ssh-connected', { id });
|
||||
}
|
||||
|
||||
// 创建交互式Shell
|
||||
conn.shell({
|
||||
term: 'xterm-256color',
|
||||
cols: 120,
|
||||
rows: 30
|
||||
}, (err, stream) => {
|
||||
if (err) {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('ssh-error', { id, error: err.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
shellStreams[id] = stream;
|
||||
|
||||
stream.on('data', (data) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('ssh-data', { id, data: data.toString('utf-8') });
|
||||
}
|
||||
});
|
||||
|
||||
stream.stderr.on('data', (data) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('ssh-data', { id, data: data.toString('utf-8') });
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('close', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('ssh-closed', { id });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化SFTP
|
||||
conn.sftp((err, sftp) => {
|
||||
if (!err) {
|
||||
sftpConnections[id] = sftp;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
conn.on('error', (err) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('ssh-error', { id, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
conn.on('close', () => {
|
||||
delete sshConnections[id];
|
||||
delete sftpConnections[id];
|
||||
delete shellStreams[id];
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('ssh-closed', { id });
|
||||
}
|
||||
});
|
||||
|
||||
const connectConfig = {
|
||||
host,
|
||||
port: port || 22,
|
||||
username,
|
||||
readyTimeout: 15000
|
||||
};
|
||||
|
||||
if (privateKey && privateKey.trim()) {
|
||||
connectConfig.privateKey = privateKey;
|
||||
} else {
|
||||
connectConfig.password = password;
|
||||
}
|
||||
|
||||
conn.connect(connectConfig);
|
||||
});
|
||||
|
||||
// SSH断开连接
|
||||
ipcMain.on('ssh-disconnect', (event, { id }) => {
|
||||
if (sshConnections[id]) {
|
||||
try {
|
||||
sshConnections[id].end();
|
||||
} catch (e) {}
|
||||
delete sshConnections[id];
|
||||
delete sftpConnections[id];
|
||||
delete shellStreams[id];
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输入
|
||||
ipcMain.on('ssh-input', (event, { id, data }) => {
|
||||
const stream = shellStreams[id];
|
||||
if (stream) {
|
||||
stream.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
// 调整终端大小
|
||||
ipcMain.on('ssh-resize', (event, { id, cols, rows }) => {
|
||||
const stream = shellStreams[id];
|
||||
if (stream) {
|
||||
stream.setWindow(rows, cols, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// ========== SFTP 文件管理 ==========
|
||||
|
||||
// 列出目录内容
|
||||
ipcMain.on('sftp-list', (event, { id, path }) => {
|
||||
const sftp = sftpConnections[id];
|
||||
if (!sftp) {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-list', { id, path, files: [], error: 'SFTP连接不存在' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sftp.readdir(path || '/', (err, list) => {
|
||||
if (err) {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-list', { id, path, files: [], error: err.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const files = list.map(item => ({
|
||||
filename: item.filename,
|
||||
longname: item.longname,
|
||||
size: item.attrs.size,
|
||||
mtime: item.attrs.mtime,
|
||||
atime: item.attrs.atime,
|
||||
permissions: item.attrs.mode,
|
||||
isDirectory: (item.attrs.mode & 0o40000) === 0o40000
|
||||
}));
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-list', { id, path, files });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 读取文件内容
|
||||
ipcMain.on('sftp-readfile', (event, { id, path }) => {
|
||||
const sftp = sftpConnections[id];
|
||||
if (!sftp) return;
|
||||
|
||||
let content = '';
|
||||
const stream = sftp.createReadStream(path, { encoding: 'utf-8' });
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
content += chunk;
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-file-content', { id, path, content });
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-file-content', { id, path, error: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 写入文件内容
|
||||
ipcMain.on('sftp-writefile', (event, { id, path, content }) => {
|
||||
const sftp = sftpConnections[id];
|
||||
if (!sftp) return;
|
||||
|
||||
const stream = sftp.createWriteStream(path);
|
||||
|
||||
stream.on('close', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-write-result', { id, path, success: true });
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-write-result', { id, path, success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
stream.end(content);
|
||||
});
|
||||
|
||||
// 创建目录
|
||||
ipcMain.on('sftp-mkdir', (event, { id, path }) => {
|
||||
const sftp = sftpConnections[id];
|
||||
if (!sftp) return;
|
||||
|
||||
sftp.mkdir(path, (err) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-mkdir-result', { id, path, success: !err, error: err?.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 删除文件/目录
|
||||
ipcMain.on('sftp-rm', (event, { id, path, isDirectory }) => {
|
||||
const sftp = sftpConnections[id];
|
||||
if (!sftp) return;
|
||||
|
||||
const callback = (err) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-rm-result', { id, path, success: !err, error: err?.message });
|
||||
}
|
||||
};
|
||||
|
||||
if (isDirectory) {
|
||||
sftp.rmdir(path, callback);
|
||||
} else {
|
||||
sftp.unlink(path, callback);
|
||||
}
|
||||
});
|
||||
|
||||
// 重命名/移动
|
||||
ipcMain.on('sftp-rename', (event, { id, oldPath, newPath }) => {
|
||||
const sftp = sftpConnections[id];
|
||||
if (!sftp) return;
|
||||
|
||||
sftp.rename(oldPath, newPath, (err) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('sftp-rename-result', { id, oldPath, newPath, success: !err, error: err?.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 系统监控 ==========
|
||||
|
||||
ipcMain.on('monitor-request', (event, { id }) => {
|
||||
const conn = sshConnections[id];
|
||||
if (!conn) return;
|
||||
|
||||
const monitorData = {
|
||||
id,
|
||||
cpu: { usage: 0, cores: '-', load: '-', model: '-' },
|
||||
memory: { percent: 0, total: '-', used: '-', free: '-' },
|
||||
disks: [],
|
||||
system: { hostname: '-', os: '-', uptime: '-', kernel: '-' },
|
||||
processes: []
|
||||
};
|
||||
|
||||
let completed = 0;
|
||||
const total = 5;
|
||||
|
||||
const checkDone = () => {
|
||||
completed++;
|
||||
if (completed >= total && mainWindow) {
|
||||
mainWindow.webContents.send('monitor-data', monitorData);
|
||||
}
|
||||
};
|
||||
|
||||
// CPU信息
|
||||
conn.exec("top -bn1 | head -5; echo '---'; nproc; echo '---'; cat /proc/loadavg; echo '---'; lscpu | grep 'Model name' | sed 's/Model name:[ ]*//'", (err, stream) => {
|
||||
if (err) { checkDone(); return; }
|
||||
let output = '';
|
||||
stream.on('data', (data) => { output += data.toString(); });
|
||||
stream.on('close', () => {
|
||||
const lines = output.split('\n');
|
||||
// 解析CPU使用率
|
||||
const cpuLine = lines.find(l => l.includes('Cpu(s)'));
|
||||
if (cpuLine) {
|
||||
const match = cpuLine.match(/([\d.]+)\s*(?:%|id)/i);
|
||||
if (match) {
|
||||
monitorData.cpu.usage = 100 - parseFloat(match[1]);
|
||||
}
|
||||
}
|
||||
// 解析核心数
|
||||
const parts = output.split('---');
|
||||
if (parts[1]) monitorData.cpu.cores = parts[1].trim();
|
||||
if (parts[2]) {
|
||||
const loads = parts[2].trim().split(/\s+/);
|
||||
monitorData.cpu.load = loads.slice(0, 3).join(', ');
|
||||
}
|
||||
if (parts[3]) monitorData.cpu.model = parts[3].trim().substring(0, 40);
|
||||
checkDone();
|
||||
});
|
||||
});
|
||||
|
||||
// 内存信息
|
||||
conn.exec("free -h | grep Mem", (err, stream) => {
|
||||
if (err) { checkDone(); return; }
|
||||
let output = '';
|
||||
stream.on('data', (data) => { output += data.toString(); });
|
||||
stream.on('close', () => {
|
||||
const parts = output.trim().split(/\s+/);
|
||||
if (parts.length >= 4) {
|
||||
monitorData.memory.total = parts[1];
|
||||
monitorData.memory.used = parts[2];
|
||||
monitorData.memory.free = parts[3];
|
||||
// 计算百分比
|
||||
const totalNum = parseFloat(parts[1]);
|
||||
const usedNum = parseFloat(parts[2]);
|
||||
if (totalNum > 0) {
|
||||
monitorData.memory.percent = (usedNum / totalNum) * 100;
|
||||
}
|
||||
}
|
||||
checkDone();
|
||||
});
|
||||
});
|
||||
|
||||
// 磁盘信息
|
||||
conn.exec("df -h | grep -E '^/dev/'", (err, stream) => {
|
||||
if (err) { checkDone(); return; }
|
||||
let output = '';
|
||||
stream.on('data', (data) => { output += data.toString(); });
|
||||
stream.on('close', () => {
|
||||
const lines = output.trim().split('\n');
|
||||
monitorData.disks = lines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
return {
|
||||
device: parts[0],
|
||||
total: parts[1],
|
||||
used: parts[2],
|
||||
available: parts[3],
|
||||
percent: parseInt(parts[4]) || 0,
|
||||
mount: parts[5]
|
||||
};
|
||||
});
|
||||
checkDone();
|
||||
});
|
||||
});
|
||||
|
||||
// 系统信息
|
||||
conn.exec("hostname; echo '---'; cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2; echo '---'; uptime -p 2>/dev/null || uptime; echo '---'; uname -r", (err, stream) => {
|
||||
if (err) { checkDone(); return; }
|
||||
let output = '';
|
||||
stream.on('data', (data) => { output += data.toString(); });
|
||||
stream.on('close', () => {
|
||||
const parts = output.split('---');
|
||||
if (parts[0]) monitorData.system.hostname = parts[0].trim();
|
||||
if (parts[1]) monitorData.system.os = parts[1].trim();
|
||||
if (parts[2]) monitorData.system.uptime = parts[2].trim().replace('up ', '');
|
||||
if (parts[3]) monitorData.system.kernel = parts[3].trim();
|
||||
checkDone();
|
||||
});
|
||||
});
|
||||
|
||||
// 进程列表
|
||||
conn.exec("ps aux --sort=-%mem | head -16 | tail -15", (err, stream) => {
|
||||
if (err) { checkDone(); return; }
|
||||
let output = '';
|
||||
stream.on('data', (data) => { output += data.toString(); });
|
||||
stream.on('close', () => {
|
||||
const lines = output.trim().split('\n');
|
||||
monitorData.processes = lines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
return {
|
||||
user: parts[0],
|
||||
pid: parts[1],
|
||||
cpu: parts[2],
|
||||
mem: parts[3],
|
||||
cmd: parts.slice(10).join(' ').substring(0, 50)
|
||||
};
|
||||
});
|
||||
checkDone();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ========== AI Agent 命令执行 ==========
|
||||
|
||||
ipcMain.on('exec-command', (event, { id, cmd, context }) => {
|
||||
const conn = sshConnections[id];
|
||||
if (!conn) {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('cmd-result', { id, output: '错误: SSH连接不存在', context });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
conn.exec(cmd, (err, stream) => {
|
||||
if (err) {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('cmd-result', { id, output: '执行错误: ' + err.message, context });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
stream.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
stream.stderr.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
stream.on('close', (code) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('cmd-result', { id, output, code, context });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user