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
@@ -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>共享盘 > <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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user