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:
1 parent
777d9e4e71
commit
76ec9387f0
202 files changed
+16555
-2543
No files matched your search
@@ -10,8 +10,9 @@ import { FitAddon } from 'xterm-addon-fit';
|
||||
import 'xterm/css/xterm.css';
|
||||
import { Terminal, Maximize2, Minimize2, X, Loader2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { attachTerminalResize, mountXterm } from '@/lib/xterm-utils';
|
||||
import { useUserStore } from '@/store';
|
||||
import i18n from '@/i18n';
|
||||
import { TerminalLoadingOverlay } from '@/components/common/TerminalLoadingOverlay';
|
||||
|
||||
interface KubectlTerminalProps {
|
||||
open: boolean;
|
||||
@@ -28,6 +29,7 @@ export function KubectlTerminal({ open, onOpenChange, clusterId, clusterName }:
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [hasOutput, setHasOutput] = useState(false);
|
||||
|
||||
// 重置全屏状态
|
||||
useEffect(() => {
|
||||
@@ -37,6 +39,7 @@ export function KubectlTerminal({ open, onOpenChange, clusterId, clusterName }:
|
||||
// 连接终端
|
||||
useEffect(() => {
|
||||
if (!open || !clusterId) return;
|
||||
setHasOutput(false);
|
||||
|
||||
const term = new XTerm({
|
||||
cursorBlink: true,
|
||||
@@ -79,40 +82,33 @@ export function KubectlTerminal({ open, onOpenChange, clusterId, clusterName }:
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.status === 0 && typeof msg.data === 'string') {
|
||||
setHasOutput(true);
|
||||
term.write(msg.data);
|
||||
}
|
||||
} catch {
|
||||
if (typeof event.data === 'string') term.write(event.data);
|
||||
if (typeof event.data === 'string') {
|
||||
setHasOutput(true);
|
||||
term.write(event.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onopen = () => {
|
||||
setConnected(true);
|
||||
setError('');
|
||||
if (containerRef.current) {
|
||||
term.open(containerRef.current);
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
fit.fit();
|
||||
} catch { /* noop */ }
|
||||
});
|
||||
// 延迟 focus:确保 Dialog 渲染完成后再聚焦终端
|
||||
setTimeout(() => {
|
||||
try {
|
||||
term.focus();
|
||||
} catch { /* noop */ }
|
||||
}, 150);
|
||||
}
|
||||
mountXterm(term, fit, () => containerRef.current, ws);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setConnected(false);
|
||||
term.write(i18n.t('kubernetes.kubectlterminal.sf4ee00d1'));
|
||||
setHasOutput(true);
|
||||
term.write('\r\n\x1b[31m[连接已断开]\x1b[0m\r\n');
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
setError(i18n.t('aiops.wsConnectFailed'));
|
||||
term.write(i18n.t('kubernetes.podexecterminal.sc8107726'));
|
||||
setError('WebSocket 连接失败');
|
||||
setHasOutput(true);
|
||||
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
|
||||
};
|
||||
|
||||
return () => {
|
||||
@@ -128,35 +124,24 @@ export function KubectlTerminal({ open, onOpenChange, clusterId, clusterName }:
|
||||
};
|
||||
}, [open, clusterId]);
|
||||
|
||||
// ResizeObserver 监听容器变化自动 fit
|
||||
// ResizeObserver 监听容器变化自动 fit,并同步远端 PTY 尺寸。 // Logan.Li
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let resizeTimer: ReturnType<typeof setTimeout>;
|
||||
const ro = new ResizeObserver(() => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
if (fitRef.current && termRef.current && wsRef.current) {
|
||||
try {
|
||||
fitRef.current.fit();
|
||||
termRef.current.focus();
|
||||
const { cols, rows } = termRef.current;
|
||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({ status: 1, cols, rows }));
|
||||
}
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
}, 60);
|
||||
});
|
||||
|
||||
ro.observe(container);
|
||||
return () => {
|
||||
clearTimeout(resizeTimer);
|
||||
ro.disconnect();
|
||||
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, wsRef.current);
|
||||
};
|
||||
}, [open]);
|
||||
attach();
|
||||
return () => cleanup?.();
|
||||
}, [open, fullscreen]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -202,11 +187,10 @@ export function KubectlTerminal({ open, onOpenChange, clusterId, clusterName }:
|
||||
</div>
|
||||
|
||||
{/* 终端容器 */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="bg-[#0f172a]"
|
||||
style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}
|
||||
/>
|
||||
<div className="relative bg-[#0f172a]" style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}>
|
||||
<div ref={containerRef} className="xterm-terminal-host h-full" />
|
||||
<TerminalLoadingOverlay show={!hasOutput && !error} label="正在连接集群..." />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user