feat: Agent采集uptime, 后端持久化kernel/uptime/diskIO, 前端磁盘BPS+IOPS展示

- Agent: system.go 添加 readUptime(), SystemStat 携带 Uptime
- Backend: Server 模型新增 Kernel 字段, ReceiveReport 持久化 kernel+uptime
- Backend: /agent/latest 返回 kernel/uptime/port_info/disk_io_info/disk_total
- Frontend: 磁盘监控改为 BPS+IOPS (4张卡片), 系统信息修复内核/运行时长/监听端口/磁盘容量
- 图表单位从 MB/s 改为 KB/s, 添加 formatBPS 工具函数
This commit is contained in:
attacker committed 2026-06-19 01:10:00 +00:00
1 parent 31c2e338a3
commit 5949ec5d0c
48 files changed
+2182 -956

No files matched your search

+93 -5
View File
@@ -9,7 +9,9 @@ import {
} from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Copy, Check, Terminal, Download, Settings, Play, Cpu, Zap, Loader2, Server } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Copy, Check, Terminal, Download, Settings, Play, Cpu, Zap, Loader2, Server, Shield } from 'lucide-react';
import { serverApi } from '@/services/api';
import { toast } from 'sonner';
@@ -17,7 +19,7 @@ interface AgentWizardProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** 可选:从服务器列表传入的目标服务器信息 */
server?: { id: number; hostname: string; ip: string } | null;
server?: { id: number; hostname: string; ip: string; has_auth?: boolean; username?: string; os?: string } | null;
/** SSH一键安装成功回调 */
onInstalled?: (serverId: number) => void;
/** SSH一键安装失败回调 */
@@ -42,6 +44,13 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed
const [showManualInput, setShowManualInput] = useState(false);
const [sshInstalling, setSshInstalling] = useState(false);
const [activeTab, setActiveTab] = useState('auto');
// 凭据输入(当服务器无认证凭据时显示)
const [sshUsername, setSshUsername] = useState('root');
const [sshPassword, setSshPassword] = useState('');
const [sshKey, setSshKey] = useState('');
const [authType, setAuthType] = useState<'password' | 'key'>('password');
const needsCreds = server && !server.has_auth;
// install.sh 通过 Nginx 代理下载(origin:80 → /agent/ → backend:8080)
const installCommand = `curl -fsSL ${origin}/agent/install.sh | bash -s -- --server ${backend} --token ${token}`;
@@ -142,11 +151,22 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed
];
// SSH 一键安装(当从服务器列表传入 server 上下文时可用)
// 如果服务器未配置凭据,将输入框中的凭据传给后端,首次连接成功后自动保存
const handleSshInstall = async () => {
if (!server || !token) return;
if (needsCreds && !sshPassword && !sshKey) {
toast.error('请填写 SSH 登录凭据(密码或密钥)');
return;
}
setSshInstalling(true);
try {
const res: any = await serverApi.installAgent(server.id, token);
const creds = needsCreds ? {
username: sshUsername,
password: sshPassword,
ssh_key: sshKey,
auth_type: authType,
} : undefined;
const res: any = await serverApi.installAgent(server.id, token, creds);
const installed = res?.installed || false;
if (installed) {
toast.success(`${server.hostname} Agent 安装成功`);
@@ -193,6 +213,12 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed
SSH一键安装
</TabsTrigger>
)}
{!server && (
<TabsTrigger value="ssh" className="data-[state=active]:bg-slate-700 opacity-50 cursor-not-allowed" disabled>
<Zap className="h-4 w-4 mr-1.5" />
SSH一键安装
</TabsTrigger>
)}
<TabsTrigger value="manual" className="data-[state=active]:bg-slate-700">
手动安装
</TabsTrigger>
@@ -240,7 +266,17 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed
</div>
</TabsContent>
{/* SSH 一键安装 — 仅当从服务器列表传入时显示 */}
{/* SSH 一键安装 — 无服务器时提示先选择 */}
{!server && (
<TabsContent value="ssh" className="mt-4 space-y-4">
<div className="text-center py-12 text-slate-400">
<Server className="h-10 w-10 mx-auto mb-3 opacity-50" />
<p className="text-sm font-medium mb-1">未选择目标主机</p>
<p className="text-xs">请先在主机列表中勾选一台主机,再点击「安装 Agent」打开此页面</p>
</div>
</TabsContent>
)}
{/* SSH 一键安装 — 有服务器时显示 */}
{server && (
<TabsContent value="ssh" className="mt-4 space-y-4">
<div className="rounded-lg bg-slate-950 border border-slate-800 p-4">
@@ -251,6 +287,58 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed
<code className="text-xs text-slate-400">{server.ip}</code>
</div>
</div>
{/* 未配置凭据时显示输入框 */}
{needsCreds && (
<div className="mb-4 p-3 rounded-lg bg-amber-950/30 border border-amber-800/40 space-y-3">
<div className="flex items-center gap-2 text-xs text-amber-300/80">
<Shield className="h-3.5 w-3.5" />
<span>该服务器未配置 SSH 凭据,请填写以下信息(首次连接成功后自动保存)</span>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-xs text-slate-400">用户名</Label>
<Input
value={sshUsername}
onChange={e => setSshUsername(e.target.value)}
placeholder="root"
className="h-8 text-sm bg-slate-900 border-slate-700 text-slate-200 mt-1"
/>
</div>
<div className="flex items-end gap-1">
<div className="flex-1">
<Label className="text-xs text-slate-400">密码</Label>
<Input
type="password"
value={sshPassword}
onChange={e => { setSshPassword(e.target.value); setAuthType('password'); }}
placeholder="******"
className="h-8 text-sm bg-slate-900 border-slate-700 text-slate-200 mt-1"
/>
</div>
<span className="text-xs text-slate-500 px-1 pb-1">或</span>
<Button variant="ghost" size="sm"
onClick={() => { setAuthType('key'); setSshPassword(''); }}
className={`text-xs h-7 px-2 ${authType==='key'?'text-blue-400':'text-slate-500'}`}>
密钥
</Button>
</div>
</div>
{authType === 'key' && (
<div>
<Label className="text-xs text-slate-400">SSH 私钥(PEM 格式)</Label>
<textarea
value={sshKey}
onChange={e => setSshKey(e.target.value)}
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...&#10;-----END OPENSSH PRIVATE KEY-----"
rows={3}
className="w-full mt-1 bg-slate-900 border border-slate-700 text-slate-200 font-mono text-xs p-2 rounded-md resize-none"
/>
</div>
)}
</div>
)}
<p className="text-sm text-slate-400 mb-4">
通过 SSH 远程连接到目标主机,自动下载并安装 Agent,完成后立即启动运行。
</p>
@@ -267,7 +355,7 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed
) : (
<>
<Zap className="h-4 w-4 mr-2" />
一键安装 Agent
{needsCreds ? '连接并安装 Agent(凭据将自动保存)' : '一键安装 Agent'}
</>
)}
</Button>