主要变更: - 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>
259 lines
8.4 KiB
TypeScript
259 lines
8.4 KiB
TypeScript
// 终端审计实时监控 Dialog — 与 WebSSH 终端弹框一致的布局与适配 // Logan.Li
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { Terminal as XTerm } from 'xterm';
|
|
import { FitAddon } from 'xterm-addon-fit';
|
|
import 'xterm/css/xterm.css';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Loader2, Maximize2, Minimize2, Monitor, X } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { attachTerminalResize, mountXterm } from '@/lib/xterm-utils';
|
|
import { useUserStore } from '@/store';
|
|
import { TerminalLoadingOverlay } from '@/components/common/TerminalLoadingOverlay';
|
|
|
|
interface SessionInfo {
|
|
session_id: number;
|
|
username: string;
|
|
server_name: string;
|
|
server_ip: string;
|
|
start_time: string;
|
|
status: string;
|
|
}
|
|
|
|
interface LiveMonitorProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
sessionId: number | null;
|
|
sessionInfo?: SessionInfo | null;
|
|
}
|
|
|
|
export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }: LiveMonitorProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const termRef = useRef<XTerm | null>(null);
|
|
const fitRef = useRef<FitAddon | null>(null);
|
|
const wsRef = useRef<WebSocket | null>(null);
|
|
const [connected, setConnected] = useState(false);
|
|
const [connectError, setConnectError] = useState('');
|
|
const [meta, setMeta] = useState<SessionInfo | null>(sessionInfo || null);
|
|
const [fullscreen, setFullscreen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setConnected(false);
|
|
setConnectError('');
|
|
setFullscreen(false);
|
|
}
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
if (!open || !sessionId) return;
|
|
|
|
setConnected(false);
|
|
setConnectError('');
|
|
|
|
const term = new XTerm({
|
|
cursorBlink: false,
|
|
fontSize: 13,
|
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
disableStdin: true,
|
|
allowProposedApi: true,
|
|
scrollback: 5000,
|
|
theme: {
|
|
background: '#0a0e14',
|
|
foreground: '#d6d6d6',
|
|
cursor: '#d6d6d6',
|
|
selectionBackground: 'rgba(255,255,255,0.2)',
|
|
},
|
|
});
|
|
|
|
const fitAddon = new FitAddon();
|
|
term.loadAddon(fitAddon);
|
|
termRef.current = term;
|
|
fitRef.current = fitAddon;
|
|
|
|
const token =
|
|
useUserStore.getState().token ||
|
|
localStorage.getItem('token') ||
|
|
(() => {
|
|
try {
|
|
const stored = JSON.parse(localStorage.getItem('user-storage') || '{}');
|
|
return stored?.state?.token || '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
})();
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${protocol}//${window.location.host}/ws/audit/${sessionId}?token=${encodeURIComponent(token)}`;
|
|
const ws = new WebSocket(wsUrl);
|
|
wsRef.current = ws;
|
|
|
|
let didOpen = false;
|
|
|
|
ws.onopen = () => {
|
|
didOpen = true;
|
|
setConnected(true);
|
|
setConnectError('');
|
|
mountXterm(term, fitAddon, () => containerRef.current);
|
|
term.writeln('\x1b[36m[审计监控] 已连接到会话...\x1b[0m');
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data);
|
|
switch (msg.type) {
|
|
case 'session_info':
|
|
setMeta(msg.payload);
|
|
term.writeln(
|
|
`\x1b[36m用户: ${msg.payload.username} 服务器: ${msg.payload.server_name} (${msg.payload.server_ip})\x1b[0m`,
|
|
);
|
|
break;
|
|
case 'history':
|
|
if (msg.data) term.write(msg.data);
|
|
break;
|
|
case 'output':
|
|
if (msg.data) term.write(msg.data);
|
|
break;
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
};
|
|
|
|
ws.onerror = () => setConnectError('WebSocket 连接失败');
|
|
|
|
ws.onclose = (e) => {
|
|
if (!didOpen) {
|
|
setConnectError(`连接失败 (code=${e.code}): ${e.reason || '认证失败或网络问题'}`);
|
|
}
|
|
setConnected(false);
|
|
term.writeln('\r\n\x1b[33m[审计监控] 连接已断开\x1b[0m');
|
|
};
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (!didOpen) {
|
|
setConnectError('连接超时 — 请确认已登录且有活跃会话');
|
|
ws.close();
|
|
}
|
|
}, 5000);
|
|
|
|
return () => {
|
|
clearTimeout(timeout);
|
|
ws.close();
|
|
term.dispose();
|
|
termRef.current = null;
|
|
fitRef.current = null;
|
|
wsRef.current = null;
|
|
setConnected(false);
|
|
};
|
|
}, [open, sessionId]);
|
|
|
|
// 容器尺寸变化时持续 fit // Logan.Li
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
let cleanup: (() => void) | undefined;
|
|
let retry = 0;
|
|
|
|
const attach = () => {
|
|
const container = containerRef.current;
|
|
const fit = fitRef.current;
|
|
const term = termRef.current;
|
|
if (!container || !fit || !term) {
|
|
if (retry++ < 30) requestAnimationFrame(attach);
|
|
return;
|
|
}
|
|
cleanup = attachTerminalResize(container, fit, term);
|
|
};
|
|
|
|
attach();
|
|
return () => cleanup?.();
|
|
}, [open, sessionId, fullscreen]);
|
|
|
|
const formatTime = (t: string) => (t ? new Date(t).toLocaleString() : '--');
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent
|
|
showCloseButton={false}
|
|
showOverlay={false}
|
|
className={cn(
|
|
'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0',
|
|
'!duration-0 !transition-none',
|
|
fullscreen
|
|
? '!max-w-none !w-screen !h-screen !rounded-none !translate-x-0 !translate-y-0 !left-0 !top-0'
|
|
: '',
|
|
)}
|
|
style={
|
|
fullscreen
|
|
? { maxWidth: '100vw', width: '100vw', height: '100vh' }
|
|
: { maxWidth: '960px', width: '92vw' }
|
|
}
|
|
>
|
|
<DialogTitle className="sr-only">终端实时监控</DialogTitle>
|
|
<DialogDescription className="sr-only">审计在线 WebSSH 会话</DialogDescription>
|
|
|
|
{/* 工具栏 — 与 TerminalDialog 一致 // Logan.Li */}
|
|
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--dg-border)]">
|
|
<div className="flex items-center gap-2 text-sm min-w-0 flex-wrap">
|
|
<Monitor className="h-4 w-4 dg-text-green shrink-0" />
|
|
<span className="text-[var(--dg-text-primary)] font-medium">终端实时监控</span>
|
|
{meta && (
|
|
<span className="text-[var(--dg-text-muted)] truncate">
|
|
{meta.username} · {meta.server_name} ({meta.server_ip}) · {formatTime(meta.start_time)}
|
|
</span>
|
|
)}
|
|
{connected && (
|
|
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10 text-xs">
|
|
● 在线
|
|
</Badge>
|
|
)}
|
|
{!connected && !connectError && (
|
|
<Badge variant="outline" className="border-yellow-500/30 dg-text-yellow bg-yellow-500/10 text-xs">
|
|
<Loader2 className="h-3 w-3 animate-spin mr-1" />
|
|
连接中
|
|
</Badge>
|
|
)}
|
|
{connectError && (
|
|
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10 text-xs">
|
|
连接失败
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
<button
|
|
type="button"
|
|
onClick={() => setFullscreen(!fullscreen)}
|
|
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
|
|
title={fullscreen ? '退出全屏' : '全屏'}
|
|
>
|
|
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onOpenChange(false)}
|
|
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{connectError && (
|
|
<p className="text-sm dg-text-red px-4 py-2 border-b border-[var(--dg-border)]">{connectError}</p>
|
|
)}
|
|
|
|
<div className="relative bg-[#0a0e14]" style={{ height: fullscreen ? 'calc(100vh - 49px)' : '520px' }}>
|
|
<div ref={containerRef} className="xterm-terminal-host h-full" />
|
|
<TerminalLoadingOverlay show={!connected && !connectError} label="正在连接监控..." />
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|