Files
Your Name edf4d743cb first add
2026-07-28 21:25:30 +08:00

881 lines
42 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSH Client Pro</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--bg-tertiary: #313244;
--bg-hover: #45475a;
--text-primary: #cdd6f4;
--text-secondary: #a6adc8;
--accent: #89b4fa;
--accent-hover: #74c7ec;
--green: #a6e3a1;
--red: #f38ba8;
--yellow: #f9e2af;
--peach: #fab387;
--mauve: #cba6f7;
--border: #45475a;
}
body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg-primary); color: var(--text-primary); height: 100vh; overflow: hidden; }
/* Layout */
.app { display: flex; height: 100vh; }
.sidebar { width: 260px; background: var(--bg-secondary); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* Sidebar */
.sidebar-header { padding: 16px; border-bottom: 1px solid var(--border); }
.sidebar-header h2 { font-size: 16px; color: var(--accent); display: flex; align-items: center; gap: 8px; }
.sidebar-header h2::before { content: '⚡'; }
.conn-list { flex: 1; overflow-y: auto; padding: 8px; }
.conn-item { padding: 10px 12px; border-radius: 8px; cursor: pointer; margin-bottom: 4px; transition: all 0.2s; display: flex; align-items: center; gap: 10px; }
.conn-item:hover { background: var(--bg-tertiary); }
.conn-item.active { background: var(--bg-tertiary); border-left: 3px solid var(--accent); }
.conn-item .status { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.conn-item .status.connected { background: var(--green); }
.conn-item .status.disconnected { background: var(--red); }
.conn-item .info { flex: 1; min-width: 0; }
.conn-item .name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.conn-item .host { font-size: 11px; color: var(--text-secondary); }
.conn-item .del-btn { opacity: 0; background: none; border: none; color: var(--red); cursor: pointer; font-size: 14px; padding: 2px 6px; border-radius: 4px; }
.conn-item:hover .del-btn { opacity: 1; }
.sidebar-footer { padding: 12px; border-top: 1px solid var(--border); }
.btn { padding: 8px 16px; border-radius: 8px; border: none; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; }
.btn-primary { background: var(--accent); color: var(--bg-primary); width: 100%; justify-content: center; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-sm { padding: 5px 10px; font-size: 12px; }
.btn-ghost { background: transparent; color: var(--text-secondary); border: 1px solid var(--border); }
.btn-ghost:hover { background: var(--bg-tertiary); color: var(--text-primary); }
.btn-danger { background: var(--red); color: #fff; }
.btn-success { background: var(--green); color: var(--bg-primary); }
/* Tabs */
.tab-bar { display: flex; background: var(--bg-secondary); border-bottom: 1px solid var(--border); padding: 0 8px; }
.tab { padding: 10px 16px; cursor: pointer; font-size: 13px; color: var(--text-secondary); border-bottom: 2px solid transparent; transition: all 0.2s; display: flex; align-items: center; gap: 6px; }
.tab:hover { color: var(--text-primary); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab-content { flex: 1; overflow: hidden; display: none; }
.tab-content.active { display: flex; flex-direction: column; }
/* Terminal */
.terminal-container { flex: 1; padding: 4px; }
.terminal-container .xterm { height: 100%; }
/* File Browser */
.file-browser { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.file-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--bg-secondary); border-bottom: 1px solid var(--border); }
.file-path { flex: 1; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 6px; padding: 6px 12px; color: var(--text-primary); font-size: 13px; font-family: 'Cascadia Code', 'Consolas', monospace; }
.file-list { flex: 1; overflow-y: auto; }
.file-row { display: grid; grid-template-columns: 32px 1fr 100px 140px 80px; align-items: center; padding: 6px 12px; border-bottom: 1px solid rgba(69,71,90,0.3); cursor: pointer; font-size: 13px; }
.file-row:hover { background: var(--bg-tertiary); }
.file-row.header { background: var(--bg-secondary); font-weight: 600; color: var(--text-secondary); font-size: 12px; cursor: default; position: sticky; top: 0; z-index: 1; }
.file-icon { font-size: 16px; }
.file-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-size { color: var(--text-secondary); text-align: right; }
.file-date { color: var(--text-secondary); font-size: 12px; }
.file-perm { color: var(--mauve); font-size: 11px; font-family: monospace; }
/* Monitor */
.monitor { flex: 1; overflow-y: auto; padding: 16px; }
.monitor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }
.monitor-card { background: var(--bg-secondary); border-radius: 12px; padding: 16px; border: 1px solid var(--border); }
.monitor-card h3 { font-size: 14px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.gauge-container { display: flex; align-items: center; gap: 16px; }
.gauge { width: 100px; height: 100px; position: relative; }
.gauge svg { transform: rotate(-90deg); }
.gauge-value { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 18px; font-weight: 700; }
.gauge-label { font-size: 12px; color: var(--text-secondary); }
.stat-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid rgba(69,71,90,0.3); font-size: 13px; }
.stat-row:last-child { border: none; }
.stat-label { color: var(--text-secondary); }
.stat-value { font-weight: 500; font-family: 'Cascadia Code', monospace; }
.chart-container { height: 120px; display: flex; align-items: flex-end; gap: 2px; margin-top: 12px; }
.chart-bar { flex: 1; background: var(--accent); border-radius: 2px 2px 0 0; min-height: 2px; transition: height 0.3s; opacity: 0.7; }
.chart-bar:last-child { opacity: 1; }
.process-table { width: 100%; font-size: 12px; border-collapse: collapse; margin-top: 8px; }
.process-table th { text-align: left; padding: 6px 8px; color: var(--text-secondary); border-bottom: 1px solid var(--border); font-weight: 500; }
.process-table td { padding: 5px 8px; border-bottom: 1px solid rgba(69,71,90,0.2); }
.process-table tr:hover td { background: var(--bg-tertiary); }
/* AI Agent */
.ai-container { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.ai-messages { flex: 1; overflow-y: auto; padding: 16px; }
.ai-msg { margin-bottom: 12px; display: flex; gap: 10px; }
.ai-msg.user { flex-direction: row-reverse; }
.ai-avatar { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.ai-msg.assistant .ai-avatar { background: var(--mauve); }
.ai-msg.user .ai-avatar { background: var(--accent); }
.ai-bubble { max-width: 70%; padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
.ai-msg.assistant .ai-bubble { background: var(--bg-tertiary); border-top-left-radius: 4px; }
.ai-msg.user .ai-bubble { background: var(--accent); color: var(--bg-primary); border-top-right-radius: 4px; }
.ai-input-area { display: flex; gap: 8px; padding: 12px; border-top: 1px solid var(--border); background: var(--bg-secondary); }
.ai-input { flex: 1; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; color: var(--text-primary); font-size: 13px; resize: none; height: 42px; font-family: inherit; }
.ai-input:focus { outline: none; border-color: var(--accent); }
.ai-suggestions { display: flex; gap: 6px; padding: 8px 12px; flex-wrap: wrap; }
.ai-suggest-btn { padding: 4px 10px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--text-secondary); font-size: 11px; cursor: pointer; }
.ai-suggest-btn:hover { background: var(--bg-tertiary); color: var(--text-primary); }
/* Modal */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal { background: var(--bg-primary); border-radius: 16px; padding: 24px; width: 420px; border: 1px solid var(--border); box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
.modal h3 { margin-bottom: 20px; font-size: 18px; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 500; }
.form-group input, .form-group select { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; color: var(--text-primary); font-size: 13px; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--accent); }
.form-row { display: flex; gap: 12px; }
.form-row .form-group { flex: 1; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
/* Welcome */
.welcome { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; }
.welcome-icon { font-size: 64px; opacity: 0.3; }
.welcome h2 { font-size: 20px; color: var(--text-secondary); }
.welcome p { color: var(--text-secondary); font-size: 14px; opacity: 0.7; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--bg-hover); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
/* Status bar */
.status-bar { display: flex; align-items: center; justify-content: space-between; padding: 4px 12px; background: var(--bg-secondary); border-top: 1px solid var(--border); font-size: 11px; color: var(--text-secondary); }
.status-bar .left, .status-bar .right { display: flex; align-items: center; gap: 12px; }
.hidden { display: none !important; }
.loading { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="app" id="app">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h2>SSH Client Pro</h2>
</div>
<div class="conn-list" id="connList"></div>
<div class="sidebar-footer">
<button class="btn btn-primary" onclick="showModal()"> 新建连接</button>
</div>
</div>
<!-- Main -->
<div class="main-content">
<div id="welcomeView" class="welcome">
<div class="welcome-icon">🖥️</div>
<h2>欢迎使用 SSH Client Pro</h2>
<p>点击左侧「新建连接」开始远程管理</p>
</div>
<div id="workView" class="hidden" style="flex:1;display:flex;flex-direction:column;">
<div class="tab-bar">
<div class="tab active" data-tab="terminal" onclick="switchTab('terminal')">🖥 终端</div>
<div class="tab" data-tab="files" onclick="switchTab('files')">📁 文件</div>
<div class="tab" data-tab="monitor" onclick="switchTab('monitor')">📊 监控</div>
<div class="tab" data-tab="ai" onclick="switchTab('ai')">🤖 AI助手</div>
</div>
<!-- Terminal Tab -->
<div class="tab-content active" id="tab-terminal">
<div class="terminal-container" id="terminalContainer"></div>
</div>
<!-- Files Tab -->
<div class="tab-content" id="tab-files">
<div class="file-browser">
<div class="file-toolbar">
<button class="btn btn-sm btn-ghost" onclick="navigateUp()">⬆ 上级</button>
<button class="btn btn-sm btn-ghost" onclick="refreshFiles()">🔄 刷新</button>
<input class="file-path" id="filePath" value="/" onkeydown="if(event.key==='Enter')navigateTo(this.value)">
<button class="btn btn-sm btn-ghost" onclick="navigateTo(document.getElementById('filePath').value)">前往</button>
</div>
<div class="file-list" id="fileList">
<div class="file-row header">
<div></div><div>名称</div><div style="text-align:right">大小</div><div>修改时间</div><div>权限</div>
</div>
</div>
</div>
</div>
<!-- Monitor Tab -->
<div class="tab-content" id="tab-monitor">
<div class="monitor" id="monitorContent">
<div class="monitor-grid">
<div class="monitor-card">
<h3>🔲 CPU 使用率</h3>
<div class="gauge-container">
<div class="gauge">
<svg width="100" height="100" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="42" fill="none" stroke="#313244" stroke-width="8"/>
<circle id="cpuGauge" cx="50" cy="50" r="42" fill="none" stroke="#89b4fa" stroke-width="8" stroke-dasharray="264" stroke-dashoffset="264" stroke-linecap="round"/>
</svg>
<div class="gauge-value" id="cpuValue">0%</div>
</div>
<div>
<div class="stat-row"><span class="stat-label">核心数</span><span class="stat-value" id="cpuCores">-</span></div>
<div class="stat-row"><span class="stat-label">负载</span><span class="stat-value" id="cpuLoad">-</span></div>
<div class="stat-row"><span class="stat-label">型号</span><span class="stat-value" id="cpuModel" style="font-size:11px">-</span></div>
</div>
</div>
<div class="chart-container" id="cpuChart"></div>
</div>
<div class="monitor-card">
<h3>💾 内存使用</h3>
<div class="gauge-container">
<div class="gauge">
<svg width="100" height="100" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="42" fill="none" stroke="#313244" stroke-width="8"/>
<circle id="memGauge" cx="50" cy="50" r="42" fill="none" stroke="#a6e3a1" stroke-width="8" stroke-dasharray="264" stroke-dashoffset="264" stroke-linecap="round"/>
</svg>
<div class="gauge-value" id="memValue">0%</div>
</div>
<div>
<div class="stat-row"><span class="stat-label">总计</span><span class="stat-value" id="memTotal">-</span></div>
<div class="stat-row"><span class="stat-label">已用</span><span class="stat-value" id="memUsed">-</span></div>
<div class="stat-row"><span class="stat-label">可用</span><span class="stat-value" id="memFree">-</span></div>
</div>
</div>
<div class="chart-container" id="memChart"></div>
</div>
<div class="monitor-card">
<h3>💿 磁盘使用</h3>
<div id="diskInfo"><p style="color:var(--text-secondary);font-size:13px">加载中...</p></div>
</div>
<div class="monitor-card">
<h3>🌐 网络 / 系统</h3>
<div id="sysInfo"><p style="color:var(--text-secondary);font-size:13px">加载中...</p></div>
</div>
<div class="monitor-card" style="grid-column: 1 / -1;">
<h3>⚙️ 进程列表 (Top 15)</h3>
<table class="process-table">
<thead><tr><th>PID</th><th>用户</th><th>CPU%</th><th>MEM%</th><th>命令</th></tr></thead>
<tbody id="processList"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- AI Tab -->
<div class="tab-content" id="tab-ai">
<div class="ai-container">
<div class="ai-messages" id="aiMessages">
<div class="ai-msg assistant">
<div class="ai-avatar">🤖</div>
<div class="ai-bubble">你好!我是AI助手,可以帮你:
• 执行远程命令并分析结果
• 诊断服务器问题
• 生成运维脚本
• 解释系统监控数据
请先连接一台服务器,然后随时向我提问!</div>
</div>
</div>
<div class="ai-suggestions">
<button class="ai-suggest-btn" onclick="aiSuggest('检查服务器负载')">检查负载</button>
<button class="ai-suggest-btn" onclick="aiSuggest('分析磁盘使用情况')">磁盘分析</button>
<button class="ai-suggest-btn" onclick="aiSuggest('查看占用内存最多的进程')">内存进程</button>
<button class="ai-suggest-btn" onclick="aiSuggest('检查系统安全日志')">安全日志</button>
<button class="ai-suggest-btn" onclick="aiSuggest('生成Nginx优化配置')">Nginx优化</button>
</div>
<div class="ai-input-area">
<textarea class="ai-input" id="aiInput" placeholder="输入指令或问题..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendAI()}"></textarea>
<button class="btn btn-primary" onclick="sendAI()" style="width:auto">发送</button>
</div>
</div>
</div>
<div class="status-bar">
<div class="left">
<span id="statusConn">未连接</span>
<span id="statusLatency"></span>
</div>
<div class="right">
<span id="statusTime"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Connection Modal -->
<div class="modal-overlay hidden" id="connModal">
<div class="modal">
<h3>🔗 新建SSH连接</h3>
<div class="form-group">
<label>连接名称</label>
<input id="fName" placeholder="My Server">
</div>
<div class="form-row">
<div class="form-group">
<label>主机地址</label>
<input id="fHost" placeholder="192.168.1.100">
</div>
<div class="form-group" style="max-width:100px">
<label>端口</label>
<input id="fPort" type="number" value="22">
</div>
</div>
<div class="form-group">
<label>用户名</label>
<input id="fUser" placeholder="root">
</div>
<div class="form-group">
<label>认证方式</label>
<select id="fAuthType" onchange="toggleAuth()">
<option value="password">密码</option>
<option value="key">私钥</option>
</select>
</div>
<div class="form-group" id="authPassword">
<label>密码</label>
<input id="fPass" type="password" placeholder="••••••••">
</div>
<div class="form-group hidden" id="authKey">
<label>私钥内容</label>
<textarea id="fKey" style="width:100%;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:10px;color:var(--text-primary);font-size:12px;height:80px;font-family:monospace;resize:vertical" placeholder="-----BEGIN RSA PRIVATE KEY-----"></textarea>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" onclick="hideModal()">取消</button>
<button class="btn btn-primary" style="width:auto" onclick="connect()">连接</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script>
// ========== State ==========
let connections = JSON.parse(localStorage.getItem('ssh_connections') || '[]');
let activeConnId = null;
let term = null;
let fitAddon = null;
let cpuHistory = [];
let memHistory = [];
let monitorTimer = null;
const { ipcRenderer } = require('electron');
// ========== Connection Management ==========
function renderConnections() {
const el = document.getElementById('connList');
el.innerHTML = connections.map(c => `
<div class="conn-item ${c.id === activeConnId ? 'active' : ''}" onclick="selectConn('${c.id}')">
<div class="status ${c.connected ? 'connected' : 'disconnected'}"></div>
<div class="info">
<div class="name">${esc(c.name)}</div>
<div class="host">${esc(c.host)}:${c.port}</div>
</div>
<button class="del-btn" onclick="event.stopPropagation();deleteConn('${c.id}')">✕</button>
</div>
`).join('');
}
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function showModal() { document.getElementById('connModal').classList.remove('hidden'); }
function hideModal() { document.getElementById('connModal').classList.add('hidden'); }
function toggleAuth() {
const t = document.getElementById('fAuthType').value;
document.getElementById('authPassword').classList.toggle('hidden', t !== 'password');
document.getElementById('authKey').classList.toggle('hidden', t !== 'key');
}
function connect() {
const name = document.getElementById('fName').value || 'Server';
const host = document.getElementById('fHost').value;
const port = parseInt(document.getElementById('fPort').value) || 22;
const username = document.getElementById('fUser').value;
const authType = document.getElementById('fAuthType').value;
const password = document.getElementById('fPass').value;
const privateKey = document.getElementById('fKey').value;
if (!host || !username) { alert('请填写主机地址和用户名'); return; }
const conn = {
id: Date.now().toString(36),
name, host, port, username, authType, password, privateKey,
connected: false
};
connections.push(conn);
saveConnections();
hideModal();
selectConn(conn.id);
}
function saveConnections() {
localStorage.setItem('ssh_connections', JSON.stringify(connections));
renderConnections();
}
function deleteConn(id) {
if (activeConnId === id) {
ipcRenderer.send('ssh-disconnect', { id });
activeConnId = null;
}
connections = connections.filter(c => c.id !== id);
saveConnections();
if (!activeConnId) {
document.getElementById('welcomeView').classList.remove('hidden');
document.getElementById('workView').classList.add('hidden');
}
}
function selectConn(id) {
activeConnId = id;
const conn = connections.find(c => c.id === id);
if (!conn) return;
renderConnections();
document.getElementById('welcomeView').classList.add('hidden');
document.getElementById('workView').classList.remove('hidden');
document.getElementById('workView').style.display = 'flex';
if (!conn.connected) {
initTerminal();
ipcRenderer.send('ssh-connect', {
id: conn.id, host: conn.host, port: conn.port,
username: conn.username, password: conn.password,
privateKey: conn.privateKey
});
document.getElementById('statusConn').textContent = `正在连接 ${conn.host}...`;
} else {
initTerminal();
document.getElementById('statusConn').textContent = `已连接: ${conn.name}`;
}
switchTab('terminal');
}
// ========== IPC Handlers ==========
ipcRenderer.on('ssh-connected', (e, data) => {
const conn = connections.find(c => c.id === data.id);
if (conn) { conn.connected = true; saveConnections(); }
if (data.id === activeConnId) {
document.getElementById('statusConn').textContent = `已连接: ${conn?.name || data.id}`;
term?.writeln('\r\n\x1b[32m✓ SSH连接成功\x1b[0m\r\n');
startMonitor();
refreshFiles();
}
});
ipcRenderer.on('ssh-error', (e, data) => {
if (data.id === activeConnId) {
term?.writeln(`\r\n\x1b[31m✕ 连接错误: ${esc(data.error)}\x1b[0m\r\n`);
document.getElementById('statusConn').textContent = '连接失败';
}
const conn = connections.find(c => c.id === data.id);
if (conn) { conn.connected = false; saveConnections(); }
});
ipcRenderer.on('ssh-data', (e, data) => {
if (data.id === activeConnId && term) {
term.write(data.data);
}
});
ipcRenderer.on('ssh-closed', (e, data) => {
const conn = connections.find(c => c.id === data.id);
if (conn) { conn.connected = false; saveConnections(); }
if (data.id === activeConnId) {
term?.writeln('\r\n\x1b[33m连接已关闭\x1b[0m\r\n');
document.getElementById('statusConn').textContent = '已断开';
stopMonitor();
}
});
ipcRenderer.on('sftp-list', (e, data) => {
if (data.id !== activeConnId) return;
renderFiles(data.files, data.path);
});
ipcRenderer.on('monitor-data', (e, data) => {
if (data.id !== activeConnId) return;
updateMonitor(data);
});
ipcRenderer.on('cmd-result', (e, data) => {
if (data.id !== activeConnId) return;
handleCmdResult(data);
});
// ========== Terminal ==========
function initTerminal() {
const container = document.getElementById('terminalContainer');
if (term) { term.dispose(); term = null; }
term = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: "'Cascadia Code', 'Consolas', 'Courier New', monospace",
theme: {
background: '#1e1e2e',
foreground: '#cdd6f4',
cursor: '#f5e0dc',
selectionBackground: '#585b7066',
black: '#45475a', red: '#f38ba8', green: '#a6e3a1', yellow: '#f9e2af',
blue: '#89b4fa', magenta: '#f5c2e7', cyan: '#94e2d5', white: '#bac2de',
brightBlack: '#585b70', brightRed: '#f38ba8', brightGreen: '#a6e3a1',
brightYellow: '#f9e2af', brightBlue: '#89b4fa', brightMagenta: '#f5c2e7',
brightCyan: '#94e2d5', brightWhite: '#a6adc8'
}
});
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(container);
fitAddon.fit();
term.onData(data => {
if (activeConnId) ipcRenderer.send('ssh-input', { id: activeConnId, data });
});
window.addEventListener('resize', () => fitAddon?.fit());
term.writeln('\x1b[36m SSH Client Pro - 终端就绪\x1b[0m');
term.writeln('\x1b[90m 等待连接...\x1b[0m\r\n');
}
// ========== Tabs ==========
function switchTab(name) {
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
document.querySelectorAll('.tab-content').forEach(t => t.classList.toggle('active', t.id === 'tab-' + name));
if (name === 'terminal') setTimeout(() => fitAddon?.fit(), 50);
if (name === 'monitor') startMonitor();
if (name === 'files') refreshFiles();
}
// ========== File Browser ==========
let currentPath = '/';
function refreshFiles() {
if (!activeConnId) return;
ipcRenderer.send('sftp-list', { id: activeConnId, path: currentPath });
}
function navigateTo(path) {
currentPath = path || '/';
document.getElementById('filePath').value = currentPath;
refreshFiles();
}
function navigateUp() {
const parts = currentPath.split('/').filter(Boolean);
parts.pop();
navigateTo('/' + parts.join('/'));
}
function renderFiles(files, path) {
currentPath = path;
document.getElementById('filePath').value = path;
const el = document.getElementById('fileList');
const sorted = [...files].sort((a, b) => {
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
return a.filename.localeCompare(b.filename);
});
el.innerHTML = `<div class="file-row header"><div></div><div>名称</div><div style="text-align:right">大小</div><div>修改时间</div><div>权限</div></div>` +
sorted.map(f => {
const icon = f.isDirectory ? '📁' : getFileIcon(f.filename);
const size = f.isDirectory ? '-' : formatSize(f.size);
const date = new Date(f.mtime * 1000).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
const perm = f.permissions ? formatPerm(f.permissions) : '-';
const click = f.isDirectory ? `ondblclick="navigateTo('${path === '/' ? '' : path}/${f.filename}')"` : '';
return `<div class="file-row" ${click}>
<div class="file-icon">${icon}</div>
<div class="file-name">${esc(f.filename)}</div>
<div class="file-size">${size}</div>
<div class="file-date">${date}</div>
<div class="file-perm">${perm}</div>
</div>`;
}).join('');
}
function getFileIcon(name) {
const ext = name.split('.').pop().toLowerCase();
const icons = { js: '📜', ts: '📜', py: '🐍', sh: '⚙️', log: '📋', json: '📋', yml: '📋', yaml: '📋', conf: '⚙️', cfg: '⚙️', html: '🌐', css: '🎨', md: '📝', txt: '📄', zip: '📦', tar: '📦', gz: '📦', jpg: '🖼', png: '🖼', sql: '🗃' };
return icons[ext] || '📄';
}
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
}
function formatPerm(mode) {
const perms = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx'];
return perms[(mode >> 6) & 7] + perms[(mode >> 3) & 7] + perms[mode & 7];
}
// ========== Monitor ==========
function startMonitor() {
stopMonitor();
requestMonitor();
monitorTimer = setInterval(requestMonitor, 3000);
}
function stopMonitor() {
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
}
function requestMonitor() {
if (!activeConnId) return;
ipcRenderer.send('monitor-request', { id: activeConnId });
}
function updateMonitor(data) {
// CPU
const cpuPct = data.cpu?.usage || 0;
document.getElementById('cpuValue').textContent = cpuPct.toFixed(1) + '%';
document.getElementById('cpuGauge').style.strokeDashoffset = 264 - (264 * cpuPct / 100);
document.getElementById('cpuGauge').style.stroke = cpuPct > 80 ? '#f38ba8' : cpuPct > 50 ? '#f9e2af' : '#89b4fa';
document.getElementById('cpuCores').textContent = data.cpu?.cores || '-';
document.getElementById('cpuLoad').textContent = data.cpu?.load || '-';
document.getElementById('cpuModel').textContent = data.cpu?.model || '-';
cpuHistory.push(cpuPct);
if (cpuHistory.length > 30) cpuHistory.shift();
renderChart('cpuChart', cpuHistory, '#89b4fa');
// Memory
const memPct = data.memory?.percent || 0;
document.getElementById('memValue').textContent = memPct.toFixed(1) + '%';
document.getElementById('memGauge').style.strokeDashoffset = 264 - (264 * memPct / 100);
document.getElementById('memGauge').style.stroke = memPct > 80 ? '#f38ba8' : memPct > 50 ? '#f9e2af' : '#a6e3a1';
document.getElementById('memTotal').textContent = data.memory?.total || '-';
document.getElementById('memUsed').textContent = data.memory?.used || '-';
document.getElementById('memFree').textContent = data.memory?.free || '-';
memHistory.push(memPct);
if (memHistory.length > 30) memHistory.shift();
renderChart('memChart', memHistory, '#a6e3a1');
// Disk
if (data.disks) {
document.getElementById('diskInfo').innerHTML = data.disks.map(d => `
<div style="margin-bottom:10px">
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">
<span>${esc(d.mount)}</span><span>${d.percent}%</span>
</div>
<div style="height:6px;background:var(--bg-tertiary);border-radius:3px;overflow:hidden">
<div style="height:100%;width:${d.percent}%;background:${d.percent > 80 ? 'var(--red)' : 'var(--accent)'};border-radius:3px"></div>
</div>
<div style="font-size:11px;color:var(--text-secondary);margin-top:2px">${d.used} / ${d.total}</div>
</div>
`).join('');
}
// System
if (data.system) {
document.getElementById('sysInfo').innerHTML = `
<div class="stat-row"><span class="stat-label">主机名</span><span class="stat-value">${esc(data.system.hostname || '-')}</span></div>
<div class="stat-row"><span class="stat-label">系统</span><span class="stat-value" style="font-size:11px">${esc(data.system.os || '-')}</span></div>
<div class="stat-row"><span class="stat-label">运行时间</span><span class="stat-value">${esc(data.system.uptime || '-')}</span></div>
<div class="stat-row"><span class="stat-label">内核</span><span class="stat-value" style="font-size:11px">${esc(data.system.kernel || '-')}</span></div>
`;
}
// Processes
if (data.processes) {
document.getElementById('processList').innerHTML = data.processes.map(p => `
<tr><td>${p.pid}</td><td>${esc(p.user)}</td><td>${p.cpu}%</td><td>${p.mem}%</td><td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(p.cmd)}</td></tr>
`).join('');
}
}
function renderChart(containerId, data, color) {
const el = document.getElementById(containerId);
const max = Math.max(...data, 1);
el.innerHTML = data.map(v => `<div class="chart-bar" style="height:${Math.max(2, v / 100 * 120)}px;background:${color}"></div>`).join('');
}
// ========== AI Agent ==========
function aiSuggest(text) {
document.getElementById('aiInput').value = text;
sendAI();
}
function sendAI() {
const input = document.getElementById('aiInput');
const text = input.value.trim();
if (!text) return;
input.value = '';
addAIMessage('user', text);
const conn = connections.find(c => c.id === activeConnId);
if (!conn?.connected) {
addAIMessage('assistant', '⚠️ 请先连接一台服务器,我才能帮你执行命令和分析。');
return;
}
// Determine if it's a command or question
const cmdPatterns = [
{ match: /检查.*负载|系统负载|load/i, cmd: 'uptime && cat /proc/loadavg', desc: '正在检查系统负载...' },
{ match: /磁盘|disk|df/i, cmd: 'df -h', desc: '正在分析磁盘使用情况...' },
{ match: /内存.*进程|占用内存|top.*mem/i, cmd: 'ps aux --sort=-%mem | head -15', desc: '正在查看内存占用最高的进程...' },
{ match: /安全.*日志|auth.*log|secure/i, cmd: 'tail -50 /var/log/auth.log 2>/dev/null || tail -50 /var/log/secure 2>/dev/null || echo "未找到安全日志"', desc: '正在检查安全日志...' },
{ match: /nginx.*优化|nginx.*config/i, cmd: 'cat /etc/nginx/nginx.conf 2>/dev/null || echo "未找到Nginx配置"', desc: '正在读取Nginx配置...' },
{ match: /cpu|处理器/i, cmd: 'lscpu | head -20 && top -bn1 | head -5', desc: '正在检查CPU信息...' },
{ match: /网络|network|netstat|端口/i, cmd: 'ss -tulnp | head -30', desc: '正在检查网络端口...' },
{ match: /docker|容器/i, cmd: 'docker ps -a 2>/dev/null || echo "Docker未安装"', desc: '正在检查Docker容器...' },
];
let matched = null;
for (const p of cmdPatterns) {
if (p.match.test(text)) { matched = p; break; }
}
if (matched) {
addAIMessage('assistant', `🔍 ${matched.desc}\n执行命令: ${matched.cmd}`);
ipcRenderer.send('exec-command', { id: activeConnId, cmd: matched.cmd, context: text });
} else {
// General AI response
const responses = {
'help': '我可以帮你:\n1. 检查服务器负载 → "检查系统负载"\n2. 分析磁盘 → "分析磁盘使用情况"\n3. 查看内存进程 → "查看占用内存最多的进程"\n4. 检查安全 → "检查系统安全日志"\n5. Nginx优化 → "生成Nginx优化配置"\n6. 网络端口 → "检查网络端口"\n7. Docker状态 → "检查Docker容器"',
};
if (/帮助|help|你能做什么/i.test(text)) {
addAIMessage('assistant', responses.help);
} else {
// Try to execute as command
addAIMessage('assistant', `💡 我将尝试执行: ${text}\n如果这不是命令,请描述你想做什么。`);
ipcRenderer.send('exec-command', { id: activeConnId, cmd: text, context: text });
}
}
}
function handleCmdResult(data) {
const output = data.output || '(无输出)';
const context = data.context || '';
let analysis = '';
// Analyze based on context
if (/负载|load/i.test(context)) {
analysis = analyzeLoad(output);
} else if (/磁盘|disk|df/i.test(context)) {
analysis = analyzeDisk(output);
} else if (/内存|mem/i.test(context)) {
analysis = analyzeMemory(output);
} else if (/安全|auth|secure/i.test(context)) {
analysis = analyzeSecurity(output);
} else if (/nginx/i.test(context)) {
analysis = analyzeNginx(output);
} else {
analysis = `📋 命令执行结果:\n\`\`\`\n${output.substring(0, 2000)}\n\`\`\``;
}
addAIMessage('assistant', analysis);
}
function analyzeLoad(output) {
const lines = output.split('\n');
let result = '📊 **系统负载分析**\n\n';
result += `原始数据:\n\`\`\`\n${output.substring(0, 500)}\n\`\`\`\n\n`;
const loadMatch = output.match(/load average[s]?:\s*([\d.]+),\s*([\d.]+),\s*([\d.]+)/i);
if (loadMatch) {
const [, l1, l5, l15] = loadMatch.map(Number);
result += `• 1分钟负载: ${l1}\n• 5分钟负载: ${l5}\n• 15分钟负载: ${l15}\n\n`;
if (l1 > 4) result += '⚠️ 负载偏高,建议检查是否有异常进程占用资源。';
else result += '✅ 系统负载正常。';
}
return result;
}
function analyzeDisk(output) {
let result = '💿 **磁盘使用分析**\n\n';
const lines = output.split('\n').filter(l => l.includes('/'));
lines.forEach(l => {
const pct = l.match(/(\d+)%/);
if (pct && parseInt(pct[1]) > 80) {
result += `⚠️ ${l.trim()}\n`;
}
});
if (!result.includes('⚠️')) result += '✅ 所有分区使用率正常。\n';
result += `\n\`\`\`\n${output.substring(0, 1000)}\n\`\`\``;
return result;
}
function analyzeMemory(output) {
let result = '💾 **内存使用分析**\n\n';
result += `Top进程:\n\`\`\`\n${output.substring(0, 1500)}\n\`\`\`\n\n`;
result += '💡 建议: 如果某进程内存占用异常,可使用 `kill -15 <PID>` 优雅终止。';
return result;
}
function analyzeSecurity(output) {
let result = '🔒 **安全日志分析**\n\n';
const failedCount = (output.match(/failed|failure|invalid/i) || []).length;
if (failedCount > 0) {
result += `⚠️ 发现 ${failedCount} 条失败/异常登录记录,建议:\n`;
result += '1. 检查是否有暴力破解尝试\n2. 考虑使用fail2ban\n3. 禁用密码登录,改用密钥认证\n\n';
} else {
result += '✅ 未发现明显异常。\n\n';
}
result += `\`\`\`\n${output.substring(0, 1500)}\n\`\`\``;
return result;
}
function analyzeNginx(output) {
let result = '🌐 **Nginx配置分析**\n\n';
if (output.includes('未找到')) {
result += '⚠️ 未找到Nginx配置文件。\n\n以下是一份优化配置建议:\n';
} else {
result += '当前配置已读取,优化建议:\n';
}
result += `
• worker_processes auto;
• worker_connections 4096;
• 启用 gzip 压缩
• 配置缓存策略
• 使用 HTTP/2
• 设置合理的 keepalive_timeout
\`\`\`nginx
worker_processes auto;
events { worker_connections 4096; }
http {
gzip on;
gzip_types text/plain application/json application/javascript text/css;
keepalive_timeout 65;
server_tokens off;
}
\`\`\``;
return result;
}
function addAIMessage(role, text) {
const el = document.getElementById('aiMessages');
const avatar = role === 'user' ? '👤' : '🤖';
el.innerHTML += `<div class="ai-msg ${role}"><div class="ai-avatar">${avatar}</div><div class="ai-bubble">${esc(text)}</div></div>`;
el.scrollTop = el.scrollHeight;
}
// ========== Status Bar Clock ==========
setInterval(() => {
document.getElementById('statusTime').textContent = new Date().toLocaleTimeString('zh-CN');
}, 1000);
// ========== Init ==========
renderConnections();
</script>
</body>
</html>