feat: 完善堡垒机与 Agent 智能运维能力

- 支持 Windows Agent 构建、安装脚本和统一上报协议

- 增强 RDP 剪贴板、虚拟盘和文件传输能力

- 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
Logan.Li committed 2026-07-05 23:49:36 +08:00
1 parent a9513c2056
commit e0d652d211
169 files changed
+12192 -9116

No files matched your search

@@ -1,23 +1,28 @@
import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import { Maximize2, Minimize2, X, Shield} from 'lucide-react';
import { cn } from '@/lib/utils';
import { attachTerminalResize, fitTerminal, mountXterm } from '@/lib/xterm-utils';
import { TerminalLoadingOverlay } from '@/components/common/TerminalLoadingOverlay';
import { useUserStore } from '@/store';
import { credentialApi, serverCredentialApi } from '@/api';
import { toast } from 'sonner';
import type { JsonData } from '@/types/common';
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Terminal as XTerm } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import "xterm/css/xterm.css";
import { Maximize2, Minimize2, X, Shield } from "lucide-react";
import { cn } from "@/lib/utils";
import {
attachTerminalResize,
fitTerminal,
mountXterm,
} from "@/lib/xterm-utils";
import { TerminalLoadingOverlay } from "@/components/common/TerminalLoadingOverlay";
import { SessionWatermark } from "@/components/common/SessionWatermark";
import { useUserStore } from "@/store";
import { credentialApi, serverCredentialApi } from "@/api";
import { toast } from "sonner";
import type { JsonData } from "@/types/common";
interface TerminalDialogProps {
open: boolean;
@@ -52,14 +57,17 @@ export function TerminalDialog({
// 凭据选择状态(hasAuth=false 时显示) // Logan.Li
const [credentials, setCredentials] = useState<JsonData[]>([]);
const [selectedCredId, setSelectedCredId] = useState<string>('');
const [selectedCredId, setSelectedCredId] = useState<string>("");
const [assigning, setAssigning] = useState(false);
const [authReady, setAuthReady] = useState(hasAuth); // 认证就绪标志
// 加载凭据列表
useEffect(() => {
if (!open || hasAuth) return;
credentialApi.list().then((res: JsonData) => setCredentials(res?.list || [])).catch(() => {});
credentialApi
.list()
.then((res: JsonData) => setCredentials(res?.list || []))
.catch(() => {});
}, [open, hasAuth]);
// 关联凭据
@@ -68,10 +76,10 @@ export function TerminalDialog({
try {
setAssigning(true);
await serverCredentialApi.assign(serverId, Number(selectedCredId));
toast.success(t('common.operationSuccess'));
toast.success(t("common.operationSuccess"));
setAuthReady(true);
} catch (e) {
toast.error(e?.message || t('common.operationFailed'));
toast.error(e?.message || t("common.operationFailed"));
} finally {
setAssigning(false);
}
@@ -81,7 +89,7 @@ export function TerminalDialog({
useEffect(() => {
if (!open) {
setAuthReady(hasAuth);
setSelectedCredId('');
setSelectedCredId("");
}
}, [open, hasAuth]);
@@ -97,10 +105,10 @@ export function TerminalDialog({
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
allowProposedApi: true,
theme: {
background: '#0a0e14',
foreground: '#d6d6d6',
cursor: '#d6d6d6',
selectionBackground: 'rgba(255,255,255,0.2)',
background: "#0a0e14",
foreground: "#d6d6d6",
cursor: "#d6d6d6",
selectionBackground: "rgba(255,255,255,0.2)",
},
});
const fit = new FitAddon();
@@ -109,8 +117,8 @@ export function TerminalDialog({
fitRef.current = fit;
// WebSocket 连接
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const token = useUserStore.getState().token || '';
const proto = location.protocol === "https:" ? "wss" : "ws";
const token = useUserStore.getState().token || "";
const wsUrl = `${proto}://${location.host}/ws/terminal?id=${serverId}&cols=80&rows=24&token=${encodeURIComponent(token)}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
@@ -124,13 +132,13 @@ export function TerminalDialog({
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.status === 0 && typeof msg.data === 'string') {
if (msg.status === 0 && typeof msg.data === "string") {
setHasOutput(true);
term.write(msg.data);
}
} catch {
setHasOutput(true);
term.write(typeof event.data === 'string' ? event.data : '');
term.write(typeof event.data === "string" ? event.data : "");
}
};
@@ -143,19 +151,21 @@ export function TerminalDialog({
ws.onclose = () => {
setConnected(false);
setHasOutput(true);
term.write(`\r\n\x1b[31m[${t('server.connectionClosed')}]\r\n`);
term.write(`\r\n\x1b[31m[${t("server.connectionClosed")}]\r\n`);
};
ws.onerror = () => {
setHasOutput(true);
term.write(`\r\n\x1b[31m[${t('server.connectionError')}]\r\n`);
term.write(`\r\n\x1b[31m[${t("server.connectionError")}]\r\n`);
};
return () => {
try {
ws.close();
term.dispose();
} catch { /* noop */ }
} catch {
/* noop */
}
termRef.current = null;
wsRef.current = null;
setConnected(false);
@@ -197,22 +207,33 @@ export function TerminalDialog({
// 禁用 Radix Dialog 默认的 ESC 关闭:终端内 vi/top 等会用到 ESC,关闭只能点头部 X 按钮 // Logan.Li
onEscapeKeyDown={(e) => e.preventDefault()}
className={cn(
'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0',
"bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0",
// 禁用 CSS 过渡:终端窗口应该即时缩放,不需要动画
'!duration-0 !transition-none',
"!duration-0 !transition-none",
fullscreen
? '!max-w-none !w-screen !h-screen !rounded-none !translate-x-0 !translate-y-0 !left-0 !top-0'
: ''
? "!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' }}
style={
fullscreen
? { maxWidth: "100vw", width: "100vw", height: "100vh" }
: { maxWidth: "960px", width: "92vw" }
}
>
<DialogTitle className="sr-only">终端 - {serverName}</DialogTitle>
<DialogDescription className="sr-only">SSH 终端连接</DialogDescription>
{/* 终端头部 */}
<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">
<div className={cn('w-2 h-2 rounded-full', connected ? 'bg-emerald-400' : 'bg-red-400')} />
<span className="text-[var(--dg-text-primary)] font-medium">{serverName}</span>
<div
className={cn(
"w-2 h-2 rounded-full",
connected ? "bg-emerald-400" : "bg-red-400",
)}
/>
<span className="text-[var(--dg-text-primary)] font-medium">
{serverName}
</span>
<span className="text-[var(--dg-text-muted)]">({serverIp})</span>
</div>
<div className="flex items-center gap-1">
@@ -220,7 +241,11 @@ export function TerminalDialog({
onClick={() => setFullscreen(!fullscreen)}
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
>
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
{fullscreen ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</button>
<button
onClick={() => onOpenChange(false)}
@@ -230,26 +255,35 @@ export function TerminalDialog({
</button>
</div>
</div>
{/* 凭据选择器(无认证凭据时显示) */}
{/* 凭据选择器(无认证凭据时显示) */}
{!authReady && (
<div className="p-6 flex flex-col items-center justify-center gap-4" style={{ height: '480px' }}>
<div
className="p-6 flex flex-col items-center justify-center gap-4"
style={{ height: "480px" }}
>
<Shield className="h-12 w-12 dg-text-yellow" />
<div className="text-center">
<p className="text-[var(--dg-text-primary)] font-medium mb-1">{t('server.noAuthCred')}</p>
<p className="text-sm text-[var(--dg-text-muted)]">{t('server.selectCredHint')}</p>
<p className="text-[var(--dg-text-primary)] font-medium mb-1">
{t("server.noAuthCred")}
</p>
<p className="text-sm text-[var(--dg-text-muted)]">
{t("server.selectCredHint")}
</p>
</div>
{credentials.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)]">{t('server.noCredAvailable')}</p>
<p className="text-xs text-[var(--dg-text-muted)]">
{t("server.noCredAvailable")}
</p>
) : (
<select
value={selectedCredId}
onChange={(e) => setSelectedCredId(e.target.value)}
className="w-64 px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
>
<option value="">{t('server.selectCred')}</option>
<option value="">{t("server.selectCred")}</option>
{credentials.map((c: JsonData) => (
<option key={c.id} value={String(c.id)}>
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
{c.type === "ssh_key" ? "🔑" : "🔒"} {c.name}
</option>
))}
</select>
@@ -259,15 +293,22 @@ export function TerminalDialog({
disabled={!selectedCredId || assigning}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{assigning ? t('server.linking') : t('server.confirmConnect')}
{assigning ? t("server.linking") : t("server.confirmConnect")}
</Button>
</div>
)}
{/* 终端容器 */}
{authReady && (
<div className="relative bg-[#0a0e14]" style={{ height: fullscreen ? 'calc(100vh - 49px)' : '520px' }}>
<div
className="relative bg-[#0a0e14]"
style={{ height: fullscreen ? "calc(100vh - 49px)" : "520px" }}
>
<div ref={containerRef} className="xterm-terminal-host h-full" />
<TerminalLoadingOverlay show={!hasOutput} label={t('server.terminalLoggingIn')} />
<SessionWatermark className="opacity-60" />
<TerminalLoadingOverlay
show={!hasOutput}
label={t("server.terminalLoggingIn")}
/>
</div>
)}
</DialogContent>