881 lines
42 KiB
HTML
881 lines
42 KiB
HTML
<!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>
|