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
@@ -29,10 +29,11 @@ import {
|
||||
import { Terminal as XTerm } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import 'xterm/css/xterm.css';
|
||||
import { attachTerminalResize, fitTerminal, mountXterm } from '@/lib/xterm-utils';
|
||||
import { TerminalLoadingOverlay } from '@/components/common/TerminalLoadingOverlay';
|
||||
import { serverApi } from '@/api';
|
||||
import { useUserStore } from '@/store';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface TerminalSession {
|
||||
id: string;
|
||||
@@ -40,6 +41,7 @@ interface TerminalSession {
|
||||
serverName: string;
|
||||
serverIp: string;
|
||||
connected: boolean;
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
interface TermEntry {
|
||||
@@ -47,6 +49,7 @@ interface TermEntry {
|
||||
ws: WebSocket;
|
||||
fit: FitAddon;
|
||||
opened: boolean;
|
||||
resizeCleanup?: () => void;
|
||||
}
|
||||
|
||||
export function WebShell() {
|
||||
@@ -67,7 +70,7 @@ export function WebShell() {
|
||||
const res: JsonData = await serverApi.getServers();
|
||||
setServers(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error(i18n.t('server.webshelltsx.s8603ff5b'), e);
|
||||
console.error('加载服务器列表失败:', e);
|
||||
setServers([]);
|
||||
}
|
||||
};
|
||||
@@ -79,8 +82,9 @@ export function WebShell() {
|
||||
// 卸载时关闭所有连接
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
termsRef.current.forEach(({ ws, term }) => {
|
||||
termsRef.current.forEach(({ ws, term, resizeCleanup }) => {
|
||||
try {
|
||||
resizeCleanup?.();
|
||||
ws.close();
|
||||
term.dispose();
|
||||
} catch {
|
||||
@@ -135,10 +139,12 @@ export function WebShell() {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.status === 0 && typeof msg.data === 'string') {
|
||||
setSessions((prev) => prev.map((s) => (s.id === sessionId ? { ...s, ready: true } : s)));
|
||||
term.write(msg.data);
|
||||
}
|
||||
} catch {
|
||||
// 非 JSON,直接写入原始数据
|
||||
setSessions((prev) => prev.map((s) => (s.id === sessionId ? { ...s, ready: true } : s)));
|
||||
term.write(typeof event.data === 'string' ? event.data : '');
|
||||
}
|
||||
};
|
||||
@@ -148,14 +154,15 @@ export function WebShell() {
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
term.write(i18n.t('server.terminaldialogt.safab2019'));
|
||||
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => (s.id === sessionId ? { ...s, connected: false } : s))
|
||||
prev.map((s) => (s.id === sessionId ? { ...s, connected: false, ready: true } : s))
|
||||
);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
term.write(i18n.t('kubernetes.podexecterminal.sc8107726'));
|
||||
setSessions((prev) => prev.map((s) => (s.id === sessionId ? { ...s, ready: true } : s)));
|
||||
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
|
||||
};
|
||||
|
||||
termsRef.current.set(sessionId, { term, ws, fit, opened: false });
|
||||
@@ -166,6 +173,7 @@ export function WebShell() {
|
||||
serverName: server.hostname,
|
||||
serverIp: server.ip,
|
||||
connected: true,
|
||||
ready: false,
|
||||
};
|
||||
setSessions((prev) => [...prev, newSession]);
|
||||
setActiveSession(sessionId);
|
||||
@@ -177,6 +185,7 @@ export function WebShell() {
|
||||
const entry = termsRef.current.get(sessionId);
|
||||
if (entry) {
|
||||
try {
|
||||
entry.resizeCleanup?.();
|
||||
if (entry.ws.readyState === WebSocket.OPEN || entry.ws.readyState === WebSocket.CONNECTING) {
|
||||
entry.ws.close();
|
||||
}
|
||||
@@ -201,12 +210,7 @@ export function WebShell() {
|
||||
const entry = termsRef.current.get(activeSession);
|
||||
if (!entry || !entry.opened) return;
|
||||
try {
|
||||
entry.fit.fit();
|
||||
const { cols, rows } = entry.term;
|
||||
entry.term.resize(cols, rows);
|
||||
if (entry.ws.readyState === WebSocket.OPEN) {
|
||||
entry.ws.send(JSON.stringify({ status: 1, cols, rows }));
|
||||
}
|
||||
fitTerminal(entry.fit, entry.term, entry.ws);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
@@ -222,7 +226,7 @@ export function WebShell() {
|
||||
if (entry && entry.opened) {
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
entry.fit.fit();
|
||||
fitTerminal(entry.fit, entry.term, entry.ws);
|
||||
entry.term.focus();
|
||||
} catch {
|
||||
/* noop */
|
||||
@@ -248,27 +252,16 @@ export function WebShell() {
|
||||
const entry = termsRef.current.get(sessionId);
|
||||
if (entry && !entry.opened) {
|
||||
try {
|
||||
entry.term.open(el);
|
||||
mountXterm(entry.term, entry.fit, () => el, entry.ws);
|
||||
entry.opened = true;
|
||||
// 延迟适配:等 DOM 布局完全确定
|
||||
setTimeout(() => {
|
||||
try { entry.fit.fit(); entry.term.focus(); } catch { /* noop */ }
|
||||
}, 100);
|
||||
// ResizeObserver 持续监听容器大小变化
|
||||
const observer = new ResizeObserver(() => {
|
||||
try { entry.fit.fit(); } catch { /* noop */ }
|
||||
});
|
||||
observer.observe(el);
|
||||
(entry as JsonData)._observer = observer;
|
||||
entry.resizeCleanup = attachTerminalResize(el, entry.fit, entry.term, entry.ws);
|
||||
} catch (e) {
|
||||
console.error(i18n.t('server.webshelltsx.s7d4b03bb'), e);
|
||||
console.error('打开终端失败:', e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const entry = termsRef.current.get(sessionId);
|
||||
if (entry && (entry as JsonData)._observer) {
|
||||
(entry as JsonData)._observer.disconnect();
|
||||
}
|
||||
entry?.resizeCleanup?.();
|
||||
containerRef.current.delete(sessionId);
|
||||
}
|
||||
};
|
||||
@@ -283,7 +276,7 @@ export function WebShell() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">WebShell</h1>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1"> {i18n.t('server.webshelltsx.s1df74087')} </p>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">远程服务器终端管理</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
@@ -295,21 +288,21 @@ export function WebShell() {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]"> {i18n.t('server.webshelltsx.s7026b318')} </DialogTitle>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">新建终端连接</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||
选择要连接的服务器
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--dg-text-secondary)]"> {i18n.t('server.webshelltsx.s030235d1')} </label>
|
||||
<label className="text-sm font-medium text-[var(--dg-text-secondary)]">选择服务器</label>
|
||||
<Select value={selectedServer} onValueChange={setSelectedServer}>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder={i18n.t('server.webshelltsx.s030235d1')} />
|
||||
<SelectValue placeholder="选择服务器" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
{runningServers.length === 0 ? (
|
||||
<SelectItem value="__none" disabled> {i18n.t('server.webshelltsx.se5460be8')} </SelectItem>
|
||||
<SelectItem value="__none" disabled>暂无运行中的服务器</SelectItem>
|
||||
) : (
|
||||
runningServers.map((server) => (
|
||||
<SelectItem key={server.id} value={server.id}>
|
||||
@@ -379,7 +372,7 @@ export function WebShell() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]"> {i18n.t('server.webshelltsx.sd5d6cbbb')} </span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">暂无连接</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -407,12 +400,19 @@ export function WebShell() {
|
||||
key={session.id}
|
||||
ref={(el) => registerContainer(session.id, el)}
|
||||
className={cn(
|
||||
'absolute inset-0',
|
||||
'absolute inset-0 xterm-terminal-host',
|
||||
activeSession === session.id ? 'block' : 'hidden'
|
||||
)}
|
||||
style={{ padding: 0 }}
|
||||
/>
|
||||
))}
|
||||
{sessions.map((session) => (
|
||||
<TerminalLoadingOverlay
|
||||
key={`${session.id}-loading`}
|
||||
show={activeSession === session.id && !session.ready}
|
||||
label="正在登录..."
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
@@ -420,8 +420,8 @@ export function WebShell() {
|
||||
style={{ height: terminalHeight }}
|
||||
>
|
||||
<Terminal className="h-16 w-16 mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium"> {i18n.t('server.webshelltsx.sefd92a06')} </p>
|
||||
<p className="text-sm mt-1"> {i18n.t('server.webshelltsx.se2e43347')} </p>
|
||||
<p className="text-lg font-medium">暂无活动连接</p>
|
||||
<p className="text-sm mt-1">点击“新建连接”开始远程会话</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -431,7 +431,7 @@ export function WebShell() {
|
||||
{activeSessionData && (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-[var(--dg-text-secondary)]"> {i18n.t('server.webshelltsx.sde027789')} </CardTitle>
|
||||
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">快捷命令</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
Reference in new issue
Block a user