591 lines
16 KiB
JavaScript
591 lines
16 KiB
JavaScript
const { app, BrowserWindow, ipcMain, Menu } = require('electron');
|
|
const path = require('path');
|
|
const { Client } = require('ssh2');
|
|
const Store = require('electron-store');
|
|
|
|
const store = new Store();
|
|
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, '../dist/index.html'));
|
|
}
|
|
|
|
mainWindow.on('closed', () => {
|
|
Object.values(sshConnections).forEach(conn => {
|
|
try { conn.end(); } catch (e) {}
|
|
});
|
|
mainWindow = null;
|
|
});
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
Menu.setApplicationMenu(null);
|
|
createWindow();
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
}
|
|
});
|
|
|
|
// ========== 连接配置持久化 ==========
|
|
|
|
ipcMain.handle('get-connections', () => {
|
|
return store.get('connections', []);
|
|
});
|
|
|
|
ipcMain.handle('save-connection', (event, config) => {
|
|
const connections = store.get('connections', []);
|
|
const idx = connections.findIndex(c => c.id === config.id);
|
|
if (idx >= 0) {
|
|
connections[idx] = config;
|
|
} else {
|
|
connections.push(config);
|
|
}
|
|
store.set('connections', connections);
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle('delete-connection', (event, id) => {
|
|
let connections = store.get('connections', []);
|
|
connections = connections.filter(c => c.id !== id);
|
|
store.set('connections', connections);
|
|
return { success: true };
|
|
});
|
|
|
|
// ========== SSH 连接管理 ==========
|
|
|
|
ipcMain.handle('ssh-connect', (event, { id, host, port, username, password, privateKey }) => {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = new Client();
|
|
let settled = false;
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (!settled) {
|
|
settled = true;
|
|
try { conn.end(); } catch (e) {}
|
|
reject(new Error('连接超时'));
|
|
}
|
|
}, 15000);
|
|
|
|
conn.on('ready', () => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timeout);
|
|
sshConnections[id] = conn;
|
|
resolve({ success: true });
|
|
});
|
|
|
|
conn.on('error', (err) => {
|
|
if (!settled) {
|
|
settled = true;
|
|
clearTimeout(timeout);
|
|
reject(new Error(err.message));
|
|
} else {
|
|
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);
|
|
});
|
|
});
|
|
|
|
ipcMain.handle('ssh-disconnect', (event, { id }) => {
|
|
if (sshConnections[id]) {
|
|
try { sshConnections[id].end(); } catch (e) {}
|
|
delete sshConnections[id];
|
|
delete sftpConnections[id];
|
|
delete shellStreams[id];
|
|
}
|
|
return { success: true };
|
|
});
|
|
|
|
// ========== Shell 会话 ==========
|
|
|
|
ipcMain.handle('ssh-shell', (event, { id }) => {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = sshConnections[id];
|
|
if (!conn) {
|
|
reject(new Error('SSH连接不存在'));
|
|
return;
|
|
}
|
|
|
|
conn.shell({
|
|
term: 'xterm-256color',
|
|
cols: 120,
|
|
rows: 30
|
|
}, (err, stream) => {
|
|
if (err) {
|
|
reject(new 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 });
|
|
}
|
|
});
|
|
|
|
resolve({ success: true });
|
|
});
|
|
});
|
|
});
|
|
|
|
// 终端输入
|
|
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.handle('sftp-init', (event, { id }) => {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = sshConnections[id];
|
|
if (!conn) {
|
|
reject(new Error('SSH连接不存在'));
|
|
return;
|
|
}
|
|
conn.sftp((err, sftp) => {
|
|
if (err) {
|
|
reject(new Error(err.message));
|
|
return;
|
|
}
|
|
sftpConnections[id] = sftp;
|
|
resolve({ success: true });
|
|
});
|
|
});
|
|
});
|
|
|
|
ipcMain.handle('sftp-readdir', (event, { id, path }) => {
|
|
return new Promise((resolve, reject) => {
|
|
const sftp = sftpConnections[id];
|
|
if (!sftp) {
|
|
reject(new Error('SFTP连接不存在'));
|
|
return;
|
|
}
|
|
|
|
sftp.readdir(path || '/', (err, list) => {
|
|
if (err) {
|
|
reject(new Error(err.message));
|
|
return;
|
|
}
|
|
|
|
const files = list.map(item => ({
|
|
filename: item.filename,
|
|
longname: item.longname,
|
|
attrs: {
|
|
size: item.attrs.size,
|
|
mtime: item.attrs.mtime,
|
|
atime: item.attrs.atime,
|
|
permissions: item.attrs.mode,
|
|
isDirectory: (item.attrs.mode & 0o40000) === 0o40000
|
|
}
|
|
}));
|
|
|
|
resolve({ success: true, files });
|
|
});
|
|
});
|
|
});
|
|
|
|
ipcMain.handle('sftp-readfile', (event, { id, path }) => {
|
|
return new Promise((resolve, reject) => {
|
|
const sftp = sftpConnections[id];
|
|
if (!sftp) {
|
|
reject(new Error('SFTP连接不存在'));
|
|
return;
|
|
}
|
|
|
|
let content = '';
|
|
const stream = sftp.createReadStream(path, { encoding: 'utf-8' });
|
|
|
|
stream.on('data', (chunk) => {
|
|
content += chunk;
|
|
});
|
|
|
|
stream.on('end', () => {
|
|
resolve({ success: true, content });
|
|
});
|
|
|
|
stream.on('error', (err) => {
|
|
reject(new Error(err.message));
|
|
});
|
|
});
|
|
});
|
|
|
|
ipcMain.handle('sftp-writefile', (event, { id, path, content }) => {
|
|
return new Promise((resolve, reject) => {
|
|
const sftp = sftpConnections[id];
|
|
if (!sftp) {
|
|
reject(new Error('SFTP连接不存在'));
|
|
return;
|
|
}
|
|
|
|
const stream = sftp.createWriteStream(path);
|
|
|
|
stream.on('close', () => {
|
|
resolve({ success: true });
|
|
});
|
|
|
|
stream.on('error', (err) => {
|
|
reject(new Error(err.message));
|
|
});
|
|
|
|
stream.end(content);
|
|
});
|
|
});
|
|
|
|
ipcMain.handle('sftp-mkdir', (event, { id, path }) => {
|
|
return new Promise((resolve, reject) => {
|
|
const sftp = sftpConnections[id];
|
|
if (!sftp) {
|
|
reject(new Error('SFTP连接不存在'));
|
|
return;
|
|
}
|
|
|
|
sftp.mkdir(path, (err) => {
|
|
if (err) {
|
|
reject(new Error(err.message));
|
|
} else {
|
|
resolve({ success: true });
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
ipcMain.handle('sftp-rm', (event, { id, path, isDirectory }) => {
|
|
return new Promise((resolve, reject) => {
|
|
const sftp = sftpConnections[id];
|
|
if (!sftp) {
|
|
reject(new Error('SFTP连接不存在'));
|
|
return;
|
|
}
|
|
|
|
const callback = (err) => {
|
|
if (err) {
|
|
reject(new Error(err.message));
|
|
} else {
|
|
resolve({ success: true });
|
|
}
|
|
};
|
|
|
|
if (isDirectory) {
|
|
sftp.rmdir(path, callback);
|
|
} else {
|
|
sftp.unlink(path, callback);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ========== 系统监控 ==========
|
|
|
|
function execRemoteCommand(conn, cmd) {
|
|
return new Promise((resolve) => {
|
|
conn.exec(cmd, (err, stream) => {
|
|
if (err) { resolve(''); return; }
|
|
let output = '';
|
|
stream.on('data', (data) => { output += data.toString(); });
|
|
stream.stderr.on('data', (data) => { output += data.toString(); });
|
|
stream.on('close', () => { resolve(output); });
|
|
});
|
|
});
|
|
}
|
|
|
|
ipcMain.handle('system-monitor', async (event, { id }) => {
|
|
const conn = sshConnections[id];
|
|
if (!conn) {
|
|
return { success: false, error: 'SSH连接不存在' };
|
|
}
|
|
|
|
try {
|
|
// 并行执行所有命令
|
|
const [cpuOutput, memOutput, diskOutput, sysOutput, processOutput, uptimeOutput] = await Promise.all([
|
|
execRemoteCommand(conn, "top -bn1 | grep 'Cpu(s)' | head -1"),
|
|
execRemoteCommand(conn, "free -m | grep Mem"),
|
|
execRemoteCommand(conn, "df -h | grep -E '^/dev/'"),
|
|
execRemoteCommand(conn, "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2; echo '---'; uname -r; echo '---'; hostname"),
|
|
execRemoteCommand(conn, "ps aux | wc -l"),
|
|
execRemoteCommand(conn, "cat /proc/loadavg; echo '---'; uptime -p 2>/dev/null || uptime")
|
|
]);
|
|
|
|
// 解析CPU使用率
|
|
let cpuUsage = 0;
|
|
const cpuMatch = cpuOutput.match(/([\d.]+)\s*id/);
|
|
if (cpuMatch) {
|
|
cpuUsage = 100 - parseFloat(cpuMatch[1]);
|
|
}
|
|
|
|
// 解析内存 (单位MB)
|
|
let memoryStr = '0,0,0,0';
|
|
const memParts = memOutput.trim().split(/\s+/);
|
|
if (memParts.length >= 7) {
|
|
const total = parseInt(memParts[1]) || 0;
|
|
const used = parseInt(memParts[2]) || 0;
|
|
const free = parseInt(memParts[3]) || 0;
|
|
const available = parseInt(memParts[6]) || 0;
|
|
memoryStr = `${total},${used},${free},${available}`;
|
|
}
|
|
|
|
// 解析磁盘
|
|
const diskLines = diskOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
const parts = line.split(/\s+/);
|
|
return `${parts[0]},${parts[1]},${parts[2]},${parts[3]},${parseInt(parts[4]) || 0},${parts[5]}`;
|
|
}).join('\n');
|
|
|
|
// 解析系统信息
|
|
const sysParts = sysOutput.split('---');
|
|
const systemInfo = [
|
|
sysParts[0]?.trim(),
|
|
sysParts[1]?.trim() ? `内核: ${sysParts[1].trim()}` : '',
|
|
sysParts[2]?.trim() ? `主机: ${sysParts[2].trim()}` : ''
|
|
].filter(Boolean).join('\n');
|
|
|
|
// 解析负载
|
|
const loadParts = uptimeOutput.split('---');
|
|
let loadStr = '0,0,0';
|
|
let uptimeStr = '-';
|
|
if (loadParts[0]) {
|
|
const loads = loadParts[0].trim().split(/\s+/);
|
|
loadStr = loads.slice(0, 3).join(',');
|
|
}
|
|
if (loadParts[1]) {
|
|
uptimeStr = loadParts[1].trim().replace('up ', '');
|
|
}
|
|
|
|
// 进程数
|
|
const processCount = parseInt(processOutput.trim()) || 0;
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
cpu: cpuUsage.toFixed(1),
|
|
memory: memoryStr,
|
|
disk: diskLines,
|
|
load: loadStr,
|
|
processes: processCount,
|
|
uptime: uptimeStr,
|
|
system: systemInfo
|
|
}
|
|
};
|
|
} catch (error) {
|
|
return { success: false, error: error.message };
|
|
}
|
|
});
|
|
|
|
// ========== AI Agent 命令执行 ==========
|
|
|
|
const TASK_COMMANDS = {
|
|
system_info: "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2; uname -a",
|
|
cpu_info: "lscpu 2>/dev/null | head -20 || cat /proc/cpuinfo | head -30",
|
|
memory_usage: "free -h",
|
|
disk_usage: "df -h",
|
|
top_processes: "ps aux --sort=-%mem | head -16",
|
|
uptime_check: "uptime",
|
|
network_info: "ip addr 2>/dev/null || ifconfig",
|
|
port_listen: "ss -tulnp 2>/dev/null || netstat -tulnp",
|
|
nginx_status: "systemctl status nginx 2>/dev/null || service nginx status 2>/dev/null || echo 'Nginx未安装'",
|
|
mysql_status: "systemctl status mysql 2>/dev/null || systemctl status mysqld 2>/dev/null || echo 'MySQL未安装'",
|
|
docker: "docker ps 2>/dev/null || echo 'Docker未安装或未运行'",
|
|
logged_users: "who",
|
|
last_logins: "last -10 2>/dev/null || echo '无法获取登录记录'",
|
|
firewall: "iptables -L -n 2>/dev/null || echo '需要root权限'",
|
|
ssh_config: "cat /etc/ssh/sshd_config | grep -v '^#' | grep -v '^$'",
|
|
sudo_users: "cat /etc/sudoers 2>/dev/null | grep -v '^#' | grep -v '^$' || echo '需要root权限'",
|
|
services: "systemctl list-units --type=service --state=running 2>/dev/null | head -20 || service --status-all 2>/dev/null",
|
|
cron_jobs: "crontab -l 2>/dev/null || echo '无定时任务'; ls /etc/cron.d/ 2>/dev/null",
|
|
env_vars: "env | sort | head -30",
|
|
package_update: "apt list --upgradable 2>/dev/null || yum check-update 2>/dev/null || echo '无法检查更新'",
|
|
disk_io: "iostat -x 1 1 2>/dev/null || echo '需要安装sysstat'"
|
|
};
|
|
|
|
ipcMain.handle('ai-execute', (event, { id, task }) => {
|
|
return new Promise((resolve) => {
|
|
const conn = sshConnections[id];
|
|
if (!conn) {
|
|
resolve({ success: false, output: '错误: SSH连接不存在' });
|
|
return;
|
|
}
|
|
|
|
// 如果是预定义任务,使用对应命令;否则直接作为命令执行
|
|
const cmd = TASK_COMMANDS[task] || task;
|
|
|
|
conn.exec(cmd, (err, stream) => {
|
|
if (err) {
|
|
resolve({ success: false, output: '执行错误: ' + err.message });
|
|
return;
|
|
}
|
|
|
|
let output = '';
|
|
stream.on('data', (data) => { output += data.toString(); });
|
|
stream.stderr.on('data', (data) => { output += data.toString(); });
|
|
stream.on('close', (code) => {
|
|
resolve({ success: code === 0, output: output || '(无输出)' });
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// ==================== 进程监控 ====================
|
|
ipcMain.handle('get-processes', async (event, { id, sortBy = 'cpu', limit = 20 }) => {
|
|
const conn = sshConnections[id];
|
|
if (!conn) {
|
|
return { success: false, error: 'SSH连接不存在' };
|
|
}
|
|
|
|
try {
|
|
const sortFlag = sortBy === 'memory' ? '--sort=-%mem' : '--sort=-%cpu';
|
|
const cmd = `ps aux ${sortFlag} | head -${limit + 1}`;
|
|
const output = await execRemoteCommand(conn, cmd);
|
|
|
|
const lines = output.trim().split('\n');
|
|
// 跳过表头
|
|
const processes = lines.slice(1).filter(Boolean).map(line => {
|
|
const parts = line.split(/\s+/);
|
|
return {
|
|
user: parts[0],
|
|
pid: parts[1],
|
|
cpu: parts[2],
|
|
mem: parts[3],
|
|
vsz: parts[4],
|
|
rss: parts[5],
|
|
stat: parts[7],
|
|
start: parts[8],
|
|
time: parts[9],
|
|
command: parts.slice(10).join(' ')
|
|
};
|
|
});
|
|
|
|
return { success: true, data: processes };
|
|
} catch (error) {
|
|
return { success: false, error: error.message };
|
|
}
|
|
});
|
|
|
|
// ==================== AI 设置 ====================
|
|
ipcMain.handle('get-ai-settings', () => {
|
|
return store.get('aiSettings', { apiKey: '', baseUrl: 'https://api.openai.com/v1', model: 'gpt-3.5-turbo' });
|
|
});
|
|
|
|
ipcMain.handle('save-ai-settings', (event, settings) => {
|
|
store.set('aiSettings', settings);
|
|
return { success: true };
|
|
});
|
|
|
|
// AI 智能命令解析
|
|
ipcMain.handle('ai-parse-command', async (event, { input, apiKey, baseUrl, model }) => {
|
|
try {
|
|
const url = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${apiKey}`
|
|
},
|
|
body: JSON.stringify({
|
|
model: model || 'gpt-3.5-turbo',
|
|
messages: [
|
|
{
|
|
role: 'system',
|
|
content: '你是一个Linux服务器管理专家。用户会用自然语言描述他们想要执行的服务器操作。你需要返回对应的Linux命令。只返回命令本身,不要任何解释、不要markdown格式、不要代码块标记。如果用户的请求不安全或无法理解,返回 #ERROR: 加上原因。'
|
|
},
|
|
{ role: 'user', content: input }
|
|
],
|
|
temperature: 0.1,
|
|
max_tokens: 200
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errText = await response.text();
|
|
return { success: false, error: `API请求失败 (${response.status}): ${errText}` };
|
|
}
|
|
|
|
const data = await response.json();
|
|
const command = data.choices[0].message.content.trim();
|
|
|
|
if (command.startsWith('#ERROR:')) {
|
|
return { success: false, error: command.replace('#ERROR:', '').trim() };
|
|
}
|
|
|
|
return { success: true, command };
|
|
} catch (error) {
|
|
return { success: false, error: `AI请求失败: ${error.message}` };
|
|
}
|
|
});
|