feat: 完善堡垒机与 Agent 智能运维能力
- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
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')}][0m\r\n`);
|
||||
term.write(`\r\n\x1b[31m[${t("server.connectionClosed")}][0m\r\n`);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
setHasOutput(true);
|
||||
term.write(`\r\n\x1b[31m[${t('server.connectionError')}][0m\r\n`);
|
||||
term.write(`\r\n\x1b[31m[${t("server.connectionError")}][0m\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>
|
||||
|
||||
Reference in new issue
Block a user