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

@@ -0,0 +1,225 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { cn } from '@/lib/utils';
import { readFromClipboard } from '@/utils/clipboard';
import { rdpFilesApi, type RdpFileItem } from '@/api/server/rdp-files';
import { Button } from '@/components/ui/button';
import { Download, FolderOpen, RefreshCw, Settings2, Upload } from 'lucide-react';
import { toast } from 'sonner';
import { RDP_VIRTUAL_DOWNLOAD_ROOT } from './rdpConstants';
type ToolsTab = 'general' | 'files';
interface RdpToolsPanelProps {
serverId: number;
remoteClipboard: string;
onSyncToRemote: (text: string) => void;
onAutoExec: (command: string) => void;
}
function fmtSize(size: number): string {
if (size >= 1 << 20) return `${(size / (1 << 20)).toFixed(1)} MB`;
if (size >= 1 << 10) return `${(size / (1 << 10)).toFixed(1)} KB`;
return `${size} B`;
}
// RDP 工具面板:通用(剪贴板)+ 文件(SMB 直传,失败时自动回退)// Logan.Li
export function RdpToolsPanel({
serverId,
remoteClipboard,
onSyncToRemote,
onAutoExec,
}: RdpToolsPanelProps) {
const [tab, setTab] = useState<ToolsTab>('general');
const [localClipDraft, setLocalClipDraft] = useState('');
const [files, setFiles] = useState<RdpFileItem[]>([]);
const [loadingFiles, setLoadingFiles] = useState(false);
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const refreshFiles = useCallback(async () => {
setLoadingFiles(true);
try {
const data = await rdpFilesApi.list(serverId);
setFiles(data?.list ?? []);
} catch (err) {
setFiles([]);
toast.error(err instanceof Error ? err.message : '无法列出远端文件(SMB 445 可能未开放)');
} finally {
setLoadingFiles(false);
}
}, [serverId]);
useEffect(() => {
if (tab === 'files') void refreshFiles();
}, [tab, refreshFiles]);
const fillLocalClipOnFocus = async () => {
const text = await readFromClipboard();
if (text) {
setLocalClipDraft(text);
return;
}
toast.info('无法自动读取剪贴板,请 Ctrl+V 粘贴(HTTPS 下可自动读取)');
};
const syncLocalClip = () => {
const trimmed = localClipDraft.trim();
if (!trimmed) {
toast.info('内容为空');
return;
}
onSyncToRemote(trimmed);
toast.success('已同步到远端');
};
const uploadWithFallback = async (file: File) => {
setUploading(true);
try {
try {
await rdpFilesApi.upload(serverId, file);
toast.success(`${file.name} 已上传到 Download`);
void refreshFiles();
return;
} catch {
// SMB 不可达时走 AIOps 暂存 + RDP 自动执行 // Logan.Li
}
const staged = await rdpFilesApi.stage(serverId, file);
onAutoExec(staged.deploy_command);
toast.success(`${file.name} 已自动下发(SMB 不可用时的回退模式)`);
} catch (err) {
toast.error(err instanceof Error ? err.message : '上传失败');
} finally {
setUploading(false);
}
};
const handleUploadPick = async (picked: File | undefined) => {
if (!picked) return;
await uploadWithFallback(picked);
setTab('files');
};
const handleDownload = async (name: string) => {
try {
await rdpFilesApi.download(serverId, name);
toast.success(`已开始下载 ${name}`);
} catch (err) {
toast.error(err instanceof Error ? err.message : '下载失败');
}
};
return (
<div className="w-[380px]">
<div className="flex border-b border-[var(--dg-border)]">
<button
type="button"
onClick={() => setTab('general')}
className={cn(
'flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs transition-colors',
tab === 'general'
? 'text-[var(--dg-text-primary)] border-b-2 border-[var(--dg-blue)]'
: 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)]',
)}
>
<Settings2 className="h-3.5 w-3.5" />
通用
</button>
<button
type="button"
onClick={() => setTab('files')}
className={cn(
'flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs transition-colors',
tab === 'files'
? 'text-[var(--dg-text-primary)] border-b-2 border-[var(--dg-blue)]'
: 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)]',
)}
>
<FolderOpen className="h-3.5 w-3.5" />
文件
</button>
</div>
{tab === 'general' && (
<div className="p-3 space-y-3">
<div className="space-y-1.5">
<label className="text-xs text-[var(--dg-text-muted)]">远端剪贴板</label>
<textarea
readOnly
value={remoteClipboard}
rows={3}
placeholder="远端复制内容将显示在此"
className="w-full resize-none rounded border border-[var(--dg-border)] bg-[var(--dg-bg-primary)] px-2 py-1.5 text-xs text-[var(--dg-text-secondary)] outline-none"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs text-[var(--dg-text-muted)]">同步到远端</label>
<textarea
value={localClipDraft}
onChange={(e) => setLocalClipDraft(e.target.value)}
onFocus={() => { void fillLocalClipOnFocus(); }}
onPaste={(e) => {
const pasted = e.clipboardData.getData('text/plain');
if (pasted) setLocalClipDraft(pasted);
}}
rows={3}
placeholder="点击输入框自动粘贴本地剪贴板"
className="w-full resize-none rounded border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-2 py-1.5 text-xs text-[var(--dg-text-primary)] outline-none"
/>
<Button type="button" size="sm" className="w-full" onClick={syncLocalClip}>
同步到远端
</Button>
</div>
</div>
)}
{tab === 'files' && (
<div className="p-3 space-y-3">
<div className="flex items-center justify-between text-xs text-[var(--dg-text-muted)]">
<span>共享盘 &gt; <span className="text-[var(--dg-text-secondary)]">Download</span></span>
<button type="button" onClick={() => { void refreshFiles(); }} className="p-1 rounded hover:bg-[var(--dg-bg-hover)]" title="刷新">
<RefreshCw className={cn('h-3.5 w-3.5', loadingFiles && 'animate-spin')} />
</button>
</div>
<code className="block text-[10px] leading-relaxed text-[var(--dg-cyan)] bg-[var(--dg-bg-primary)] rounded px-2 py-1.5 break-all">
{RDP_VIRTUAL_DOWNLOAD_ROOT}
</code>
<input
ref={fileInputRef}
type="file"
className="hidden"
onChange={(e) => {
void handleUploadPick(e.target.files?.[0]);
e.currentTarget.value = '';
}}
/>
<Button type="button" size="sm" className="w-full" disabled={uploading} onClick={() => fileInputRef.current?.click()}>
<Upload className="h-3.5 w-3.5 mr-1.5" />
{uploading ? '上传中...' : '上传文件'}
</Button>
<div className="max-h-40 overflow-y-auto rounded border border-[var(--dg-border)]">
{files.length === 0 ? (
<p className="px-2 py-3 text-[10px] text-[var(--dg-text-muted)] text-center">
{loadingFiles ? '加载中...' : '暂无文件(需 SMB 445 可达才能列出)'}
</p>
) : (
files.map((f) => (
<div key={f.name} className="flex items-center justify-between gap-2 px-2 py-1.5 text-xs border-b border-[var(--dg-border)] last:border-0 hover:bg-[var(--dg-bg-hover)]">
<div className="min-w-0 flex-1">
<p className="truncate text-[var(--dg-text-primary)]">{f.name}</p>
<p className="text-[10px] text-[var(--dg-text-muted)]">{fmtSize(f.size)}</p>
</div>
<button type="button" onClick={() => { void handleDownload(f.name); }} className="p-1 rounded hover:bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)]" title="下载">
<Download className="h-3.5 w-3.5" />
</button>
</div>
))
)}
</div>
<p className="text-[10px] text-[var(--dg-text-muted)] leading-relaxed">
优先 SMB 直传(一键);若 445 不通则自动暂存到 AIOps 并由 RDP 执行下载命令。
</p>
</div>
)}
</div>
);
}