feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物

主要变更:
- RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、
  backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API
- Windows SMB 文件传输:backend/pkg/winsmb
- 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情
- Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs
- 服务器导入助手:server_import_helpers、ImportCredentialSettings
- 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块)
- 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-*
- .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
LoganandClaude committed 2026-07-04 01:50:36 +08:00
1 parent 777d9e4e71
commit 76ec9387f0
202 files changed
+16555 -2543

No files matched your search

@@ -1,11 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus + 自定义仪表盘 // Logan.Li
// 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus // Logan.Li
import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Monitor,
Plus,
RefreshCw,
Maximize2,
Minimize2,
@@ -29,7 +28,6 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
@@ -37,8 +35,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
import { useUserStore } from '@/store';
import { dashboardApi, promDatasourceApi, zabbixDatasourceApi, agentApi, serverApi } from '@/api';
import { GrafanaPanelContent } from './GrafanaPanelContent';
import { promDatasourceApi, zabbixDatasourceApi, agentApi, serverApi } from '@/api';
import { ZabbixDashboard } from './ZabbixDashboard';
import { PrometheusDashboard } from './PrometheusDashboard';
import { asList } from '@/lib/agent-utils';
@@ -52,12 +49,11 @@ import { PageLoading } from '@/components/layout/PageLoading';
import { PageEmpty } from '@/components/layout/PageEmpty';
// ═══════════ Tab 配置 ═══════════
type TabId = 'agent' | 'zabbix' | 'prometheus' | 'dashboard';
type TabId = 'agent' | 'zabbix' | 'prometheus';
const getTabs = (t: (key: string) => string): { id: TabId; label: string; icon: React.ReactNode }[] => [
{ id: 'agent', label: t('monitor.dashboard.tab.agent'), icon: <Server className="w-3.5 h-3.5" /> },
{ id: 'zabbix', label: 'Zabbix', icon: <Activity className="w-3.5 h-3.5" /> },
{ id: 'prometheus', label: 'Prometheus', icon: <Activity className="w-3.5 h-3.5" /> },
{ id: 'dashboard', label: t('monitor.dashboard.tab.dashboard'), icon: <Monitor className="w-3.5 h-3.5" /> },
];
// ═══════════ Agent 主机监控表格 ═══════════
@@ -426,7 +422,11 @@ const AgentTab: React.FC = () => {
<td className="px-2 py-2.5">
<Checkbox checked={selected.has(String(h.server_id))} onCheckedChange={(c) => {
const next = new Set(selected);
c ? next.add(String(h.server_id)) : next.delete(String(h.server_id));
if (c) {
next.add(String(h.server_id));
} else {
next.delete(String(h.server_id));
}
setSelected(next);
}} />
</td>
@@ -480,16 +480,65 @@ const AgentTab: React.FC = () => {
};
// ═══════════ 数据源 Wrapper ═══════════
interface MonitorDatasource {
id: number | string;
name?: string;
url: string;
username?: string;
password?: string;
is_default?: boolean;
}
const normalizeDatasourceList = (r: any): MonitorDatasource[] => {
const list = (r?.data || r || []) as MonitorDatasource[];
return [...list].sort((a, b) => {
if (!!a.is_default !== !!b.is_default) return a.is_default ? -1 : 1;
return String(a.name || a.url).localeCompare(String(b.name || b.url));
});
};
const DatasourceToolbar: React.FC<{
sources: MonitorDatasource[];
activeId: string;
onChange: (id: string) => void;
onRefresh: () => void;
refreshing?: boolean;
}> = ({ sources, activeId, onChange, onRefresh, refreshing }) => (
<div className="flex items-center gap-2 mb-3 flex-wrap">
<Select value={activeId} onValueChange={onChange}>
<SelectTrigger className="w-[280px] h-8 text-xs dg-input">
<SelectValue placeholder="选择数据源" />
</SelectTrigger>
<SelectContent className="dg-input">
{sources.map(ds => (
<SelectItem key={String(ds.id)} value={String(ds.id)}>
{ds.name || ds.url}{ds.is_default ? ' · 默认' : ''}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" size="sm" className="h-8 text-xs gap-1" onClick={onRefresh} disabled={refreshing}>
<RefreshCw className={cn('w-3 h-3', refreshing && 'animate-spin')} />刷新数据源
</Button>
<span className="text-[11px] text-[var(--dg-text-muted)]">共 {sources.length} 个数据源,当前仅查询选中项</span>
</div>
);
const ZabbixTab: React.FC = () => {
const { t } = useTranslation();
const [sources, setSources] = useState<MonitorDatasource[]>([]);
const [ds, setDs] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
zabbixDatasourceApi.list().then(( r: any) => {
const list = r?.data || r || [];
setDs(list.find(( d: any) => d.is_default) || list[0] || null);
}).catch(() => {}).finally(() => setLoading(false));
}, []);
const fetchSources = async () => {
setLoading(true);
try {
const list = normalizeDatasourceList(await zabbixDatasourceApi.list());
setSources(list);
setDs(current => list.find(d => String(d.id) === String(current?.id)) || list[0] || null);
} catch { setSources([]); setDs(null); }
finally { setLoading(false); }
};
useEffect(() => { fetchSources(); }, []);
if (loading) return <PageLoading label={t('monitor.dashboard.zabbix.loading')} />;
if (!ds) return (
<PageEmpty
@@ -498,19 +547,29 @@ const ZabbixTab: React.FC = () => {
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">{t('monitor.dashboard.zabbix.goConfig')} <ExternalLink className="w-3 h-3" /></a>}
/>
);
return <ZabbixDashboard dsUrl={ds.url} dsUsername={ds.username} dsPassword={ds.password} />;
return (
<div>
<DatasourceToolbar sources={sources} activeId={String(ds.id)} onChange={(id) => setDs(sources.find(item => String(item.id) === id) || ds)} onRefresh={fetchSources} refreshing={loading} />
<ZabbixDashboard key={String(ds.id)} dsUrl={ds.url} dsUsername={ds.username} dsPassword={ds.password} />
</div>
);
};
const PrometheusTab: React.FC = () => {
const { t } = useTranslation();
const [sources, setSources] = useState<MonitorDatasource[]>([]);
const [ds, setDs] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
promDatasourceApi.list().then(( r: any) => {
const list = r?.data || r || [];
setDs(list.find(( d: any) => d.is_default) || list[0] || null);
}).catch(() => {}).finally(() => setLoading(false));
}, []);
const fetchSources = async () => {
setLoading(true);
try {
const list = normalizeDatasourceList(await promDatasourceApi.list());
setSources(list);
setDs(current => list.find(d => String(d.id) === String(current?.id)) || list[0] || null);
} catch { setSources([]); setDs(null); }
finally { setLoading(false); }
};
useEffect(() => { fetchSources(); }, []);
if (loading) return <PageLoading label={t('monitor.dashboard.prometheus.loading')} />;
if (!ds) return (
<PageEmpty
@@ -519,92 +578,10 @@ const PrometheusTab: React.FC = () => {
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">{t('monitor.dashboard.zabbix.goConfig')} <ExternalLink className="w-3 h-3" /></a>}
/>
);
return <PrometheusDashboard dsUrl={ds.url} />;
};
// ═══════════ 自定义仪表盘 ═══════════
interface Dashboard { id: string; name: string; description: string; datasource: string; datasourceUrl: string; panels: Panel[]; createdAt: string; updatedAt: string; createdBy: string; isDefault: boolean; }
export interface Panel { id: string; title: string; type: string; query: string; datasource: string; width: number; height: number; x: number; y: number; options?: any; }
const DashboardTab: React.FC = () => {
const { t } = useTranslation();
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
const [active, setActive] = useState<Dashboard | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [addOpen, setAddOpen] = useState(false);
const [nd, setNd] = useState({ name: '', description: '', datasource: 'prometheus' as const, datasourceUrl: '' });
const [np, setNp] = useState({ title: '', type: 'graph' as const, query: '', width: 6, height: 4 });
const [refreshing, setRefreshing] = useState(false);
const fetch = async () => {
setRefreshing(true);
try {
const res = await dashboardApi.getDashboards();
const list = res.data || [];
setDashboards(list);
if (list.length > 0 && !active) setActive(list[0]);
} catch (e: any) {
toast.error(t('monitor.dashboard.custom.toastRefreshFailed', { msg: (e?.message || t('monitor.dashboard.agent.toast.unknownError')) }));
} finally {
setRefreshing(false);
}
};
useEffect(() => { fetch(); }, []);
const create = async () => {
if (!nd.name) { toast.error(t('monitor.dashboard.custom.toastNameRequired')); return; }
try { await dashboardApi.createDashboard({ ...nd, panels: [] }); toast.success(t('monitor.dashboard.custom.toastCreateSuccess')); setCreateOpen(false); setNd({ name: '', description: '', datasource: 'prometheus', datasourceUrl: '' }); fetch(); } catch { toast.error(t('monitor.dashboard.custom.toastCreateFailed')); }
};
const addPanel = async () => {
if (!np.title || !np.query || !active) return;
try {
const p: Panel = { id: Date.now().toString(), ...np, datasource: active.datasource, x: 0, y: 0, options: { legend: true, tooltip: true, refreshInterval: 30 } };
await dashboardApi.updateDashboard(active.id, { ...active, panels: [...active.panels, p] });
toast.success(t('monitor.dashboard.custom.toastPanelAdded')); setAddOpen(false); setNp({ title: '', type: 'graph', query: '', width: 6, height: 4 }); fetch();
} catch { toast.error(t('monitor.dashboard.custom.toastPanelFailed')); }
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Select value={active?.id} onValueChange={id => { const d = dashboards.find(x => x.id === id); if (d) setActive(d); }}>
<SelectTrigger className="w-[240px] h-8 text-xs"><SelectValue placeholder={t('monitor.dashboard.custom.selectDashboard')} /></SelectTrigger>
<SelectContent>{dashboards.map(d => <SelectItem key={d.id} value={d.id}>{d.name}{d.isDefault ? ` ${t('monitor.dashboard.custom.defaultTag')}` : ''}</SelectItem>)}</SelectContent>
</Select>
<Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={fetch} disabled={refreshing}>
<RefreshCw className={cn('w-3 h-3 mr-1', refreshing && 'animate-spin')} />{t('monitor.dashboard.refresh')}
</Button>
{active && <Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setAddOpen(true)}><Plus className="w-3 h-3 mr-1" />{t('monitor.dashboard.custom.addPanel')}</Button>}
<Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setCreateOpen(true)}><Plus className="w-3 h-3 mr-1" />{t('monitor.dashboard.custom.create')}</Button>
</div>
{active ? (
active.panels.length === 0 ? (
<PageEmpty icon={<BarChart3 className="w-10 h-10" />} title={t('monitor.dashboard.custom.emptyNoPanels')} className="py-16" />
) : (
<div className="grid grid-cols-12 gap-3">
{active.panels.map(p => (
<Card key={p.id} style={{ gridColumn: `span ${p.width}`, minHeight: `${p.height * 80}px` }}>
<CardHeader className="py-2 px-3"><CardTitle className="text-[11px] font-medium">{p.title}</CardTitle></CardHeader>
<CardContent className="p-3 pt-0"><GrafanaPanelContent panel={p} datasourceUrl={active.datasourceUrl} /></CardContent>
</Card>
))}
</div>
)
) : (
<PageEmpty icon={<Monitor className="w-10 h-10" />} title={t('monitor.dashboard.custom.emptyNoDashboards')} className="py-16" />
)}
<Dialog open={createOpen} onOpenChange={setCreateOpen}><DialogContent><DialogHeader><DialogTitle>{t('monitor.dashboard.custom.createTitle')}</DialogTitle></DialogHeader>
<div className="space-y-3"><div><Label>{t('monitor.dashboard.custom.name')}</Label><Input value={nd.name} onChange={e => setNd({ ...nd, name: e.target.value })} /></div>
<div><Label>{t('monitor.dashboard.custom.datasource')}</Label><Select value={nd.datasource} onValueChange={v => setNd({ ...nd, datasource: v as any })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="prometheus">Prometheus</SelectItem><SelectItem value="zabbix">Zabbix</SelectItem><SelectItem value="skywalking">SkyWalking</SelectItem><SelectItem value="custom">Custom</SelectItem></SelectContent></Select></div></div>
<DialogFooter><Button variant="outline" onClick={() => setCreateOpen(false)}>{t('monitor.dashboard.custom.cancel')}</Button><Button onClick={create}>{t('monitor.dashboard.custom.createBtn')}</Button></DialogFooter></DialogContent></Dialog>
<Dialog open={addOpen} onOpenChange={setAddOpen}><DialogContent><DialogHeader><DialogTitle>{t('monitor.dashboard.custom.addPanelTitle')}</DialogTitle></DialogHeader>
<div className="space-y-3"><div><Label>{t('monitor.dashboard.custom.panelTitle')}</Label><Input value={np.title} onChange={e => setNp({ ...np, title: e.target.value })} /></div>
<div><Label>{t('monitor.dashboard.custom.panelType')}</Label><Select value={np.type} onValueChange={v => setNp({ ...np, type: v as any })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="graph">{t('monitor.dashboard.custom.typeGraph')}</SelectItem><SelectItem value="stat">{t('monitor.dashboard.custom.typeStat')}</SelectItem><SelectItem value="gauge">{t('monitor.dashboard.custom.typeGauge')}</SelectItem><SelectItem value="table">{t('monitor.dashboard.custom.typeTable')}</SelectItem></SelectContent></Select></div>
<div><Label>{t('monitor.dashboard.custom.panelQuery')}</Label><Input value={np.query} onChange={e => setNp({ ...np, query: e.target.value })} /></div></div>
<DialogFooter><Button variant="outline" onClick={() => setAddOpen(false)}>{t('monitor.dashboard.custom.cancel')}</Button><Button onClick={addPanel}>{t('monitor.dashboard.custom.addBtn')}</Button></DialogFooter></DialogContent></Dialog>
<div>
<DatasourceToolbar sources={sources} activeId={String(ds.id)} onChange={(id) => setDs(sources.find(item => String(item.id) === id) || ds)} onRefresh={fetchSources} refreshing={loading} />
<PrometheusDashboard key={String(ds.id)} dsUrl={ds.url} />
</div>
);
};
@@ -638,7 +615,6 @@ const GrafanaDashboard: React.FC = () => {
{tab === 'agent' && <AgentTab />}
{tab === 'zabbix' && <ZabbixTab />}
{tab === 'prometheus' && <PrometheusTab />}
{tab === 'dashboard' && <DashboardTab />}
</PageShell>
);
};