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

@@ -20,9 +20,10 @@ import {
Terminal, Maximize2, Minimize2, X, Loader2, RotateCw, Box,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { attachTerminalResize, mountXterm } from '@/lib/xterm-utils';
import { TerminalLoadingOverlay } from '@/components/common/TerminalLoadingOverlay';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import i18n from '@/i18n';
export interface PodExecTerminalProps {
open: boolean;
@@ -62,6 +63,7 @@ export function PodExecTerminal({
const [fullscreen, setFullscreen] = useState(false);
const [container, setContainer] = useState<string>(initialContainer || containers[0] || '');
const [shell, setShell] = useState<string>('sh');
const [hasOutput, setHasOutput] = useState(false);
// ─── 重置 ───
useEffect(() => {
@@ -92,6 +94,7 @@ export function PodExecTerminal({
}
setConnState('connecting');
setHasOutput(false);
term.clear();
term.writeln(`\x1b[36m[正在连接 ${podName}${container ? '/' + container : ''} shell=${shell} ...]\x1b[0m`);
@@ -115,32 +118,39 @@ export function PodExecTerminal({
try {
const msg = JSON.parse(event.data);
if (msg.status === 0 && typeof msg.data === 'string') {
setHasOutput(true);
term.write(msg.data);
} else if (msg.status === 2 || msg.error) {
const errMsg = msg.data || msg.message || msg.error || i18n.t('kubernetes.podexecterminal.s05f6e923');
setHasOutput(true);
const errMsg = msg.data || msg.message || msg.error || '执行错误';
if (errMsg.includes('not found') && shell === 'bash') {
term.writeln(i18n.t('kubernetes.podexecterminal.s14481be5'));
term.writeln('\r\n\x1b[33m⚠ bash 不可用,请切换 /bin/sh 后重连。\x1b[0m');
errorShownRef.current = true;
return;
}
term.write(errMsg.includes('\x1b') ? errMsg : `\r\n\x1b[31m${errMsg}\x1b[0m\r\n`);
}
} catch {
if (typeof event.data === 'string') term.write(event.data);
if (typeof event.data === 'string') {
setHasOutput(true);
term.write(event.data);
}
}
};
ws.onclose = (_ev) => {
setConnState((s) => (s === 'error' ? s : 'closed'));
setHasOutput(true);
if (!errorShownRef.current) {
term.write(i18n.t('kubernetes.podexecterminal.s123d9435'));
term.write('\r\n\x1b[33m[连接已关闭]\x1b[0m\r\n');
}
};
ws.onerror = (ev) => {
console.error('[PodExec:connect] WS error event fired, readyState=', ws.readyState, 'type=', ev.type);
setConnState('error');
term.write(i18n.t('kubernetes.podexecterminal.sc8107726'));
setHasOutput(true);
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
};
}, [clusterId, namespace, podName, container, shell]);
@@ -200,8 +210,7 @@ export function PodExecTerminal({
}
});
term.open(el);
requestAnimationFrame(() => { try { fit.fit(); } catch { /* noop */ } });
mountXterm(term, fit, () => el, wsRef.current);
mountedRef.current = true;
connect();
@@ -232,45 +241,37 @@ export function PodExecTerminal({
useEffect(() => {
if (open && termRef.current && mountedRef.current) {
connect();
} else {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [container, shell]);
// ─── ResizeObserver ───
// ─── ResizeObserver:同步容器尺寸和远端 PTY 尺寸 // Logan.Li ───
useEffect(() => {
if (!open) return;
const el = containerRef.current;
if (!el) return;
let timer: ReturnType<typeof setTimeout>;
const ro = new ResizeObserver(() => {
clearTimeout(timer);
timer = setTimeout(() => {
if (fitRef.current && termRef.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(el);
return () => { clearTimeout(timer); 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);
};
attach();
return () => cleanup?.();
}, [open, fullscreen]);
// ─── UI ───
const statusColor = connState === 'connected' ? 'bg-emerald-400'
: connState === 'error' ? 'bg-red-400'
: connState === 'closed' ? 'bg-[var(--dg-bg-active)]' : 'bg-amber-400';
const statusText = connState === 'connected' ? i18n.t('agent.agentConnected')
: connState === 'connecting' ? i18n.t('common.connecting')
: connState === 'error' ? i18n.t('kubernetes.podexecterminal.sd5235918')
: connState === 'closed' ? i18n.t('agent.agentDisconnected') : i18n.t('common.idle');
const statusText = connState === 'connected' ? '已连接'
: connState === 'connecting' ? '连接中'
: connState === 'error' ? '连接错误'
: connState === 'closed' ? '已断开' : '空闲';
const handleTerminalClick = useCallback(() => {
try { termRef.current?.focus(); } catch { /* noop */ }
@@ -290,7 +291,7 @@ export function PodExecTerminal({
style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '960px', width: '92vw' }}
>
<DialogTitle className="sr-only">exec — {podName}</DialogTitle>
<DialogDescription className="sr-only"> {i18n.t('kubernetes.podexecterminal.s1a62d44d')} </DialogDescription>
<DialogDescription className="sr-only">进入 Pod 容器执行命令</DialogDescription>
{/* 工具栏 */}
<div className="flex items-center justify-between gap-2 px-4 py-2 border-b border-[var(--dg-border)] flex-wrap">
@@ -306,7 +307,7 @@ export function PodExecTerminal({
<Select value={container} onValueChange={setContainer}>
<SelectTrigger className="h-8 w-[150px] text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)]">
<Box className="h-3 w-3 mr-1 text-[var(--dg-text-muted)]" />
<SelectValue placeholder={i18n.t('kubernetes.container.title')} />
<SelectValue placeholder="容器" />
</SelectTrigger>
<SelectContent>{containers.map((c) => <SelectItem key={c} value={c}>{c}</SelectItem>)}</SelectContent>
</Select>
@@ -318,25 +319,27 @@ export function PodExecTerminal({
<SelectContent>{SHELLS.map(s => <SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>)}</SelectContent>
</Select>
<Button variant="ghost" size="sm" className="h-8 px-2 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
onClick={() => { connect(); toast({ title: i18n.t('kubernetes.podexecterminal.sad127ce8') }); }} title={i18n.t('kubernetes.podexecterminal.s114d0e25')}>
onClick={() => { connect(); toast({ title: '正在重连…' }); }} title="重连">
<RotateCw className="h-3.5 w-3.5" />
</Button>
<button onClick={() => setFullscreen(!fullscreen)} className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]" title={fullscreen ? i18n.t('common.exitFullscreen2') : i18n.t('monitor.fullscreen')}>
<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 onClick={() => onOpenChange(false)} className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]" title={i18n.t('agent.portClosed')}>
<button onClick={() => onOpenChange(false)} className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]" title="关闭">
<X className="h-4 w-4" />
</button>
</div>
</div>
{/* 终端容器 */}
<div
ref={containerRef}
className="bg-[#0f172a] cursor-text"
style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}
onClick={handleTerminalClick}
/>
<div className="relative bg-[#0f172a]" style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}>
<div
ref={containerRef}
className="xterm-terminal-host h-full cursor-text"
onClick={handleTerminalClick}
/>
<TerminalLoadingOverlay show={connState === 'connecting' && !hasOutput} label="正在进入容器..." />
</div>
</DialogContent>
</Dialog>
);