feat: Agent CLI 与终端审计回放全面优化

- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏
- 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复
- 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示
- 审计历史页只读(移除删除),移除回放弹框录制搜索
This commit is contained in:
Logan committed 2026-07-01 12:13:53 +08:00
1 parent 9a3dd304f7
commit 1acb0c49af
81 files changed
+3916 -1655

No files matched your search

@@ -50,33 +50,33 @@ export function KubernetesNodes() {
const getRoleBadge = (role: string) =>
role === 'master'
? <Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30">Master</Badge>
: <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">Worker</Badge>;
? <Badge className="bg-[var(--dg-blue)]/20 text-[var(--dg-blue)] border-[var(--dg-blue)]/30">Master</Badge>
: <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">Worker</Badge>;
const getStatusBadge = (status: string) =>
status === 'Ready'
? <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">{status}</Badge>
? <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">{status}</Badge>
: <Badge variant="destructive">{status || 'Unknown'}</Badge>;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">节点管理</h1>
<p className="text-sm text-slate-500 mt-1">Kubernetes 集群工作节点状态与资源</p>
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">节点管理</h1>
<p className="text-sm text-[var(--dg-text-muted)] mt-1">Kubernetes 集群工作节点状态与资源</p>
</div>
<div className="flex items-center gap-2">
<Select value={selectedCluster} onValueChange={setSelectedCluster}>
<SelectTrigger className="w-48 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
<SelectTrigger className="w-48 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue placeholder="选择集群" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
{clusters.map((c) => (
<SelectItem key={c.id} value={String(c.code || c.id)}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400" onClick={loadNodes} disabled={loading}>
<Button variant="outline" size="icon" className="h-9 w-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={loadNodes} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
</Button>
</div>
@@ -84,14 +84,14 @@ export function KubernetesNodes() {
<div className="flex items-center gap-3">
<div className="relative w-56">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input placeholder="搜索节点..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
<Input placeholder="搜索节点..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
</div>
<Select value={roleFilter} onValueChange={setRoleFilter}>
<SelectTrigger className="w-32 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
<SelectTrigger className="w-32 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
<SelectItem value="all">全部角色</SelectItem>
<SelectItem value="master">Master</SelectItem>
<SelectItem value="worker">Worker</SelectItem>
@@ -99,54 +99,54 @@ export function KubernetesNodes() {
</Select>
</div>
<Card className="bg-slate-900 border-slate-800">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">节点名称</TableHead>
<TableHead className="text-slate-400">IP</TableHead>
<TableHead className="text-slate-400">角色</TableHead>
<TableHead className="text-slate-400">状态</TableHead>
<TableHead className="text-slate-400">配置</TableHead>
<TableHead className="text-slate-400">使用率</TableHead>
<TableHead className="text-slate-400">Pods</TableHead>
<TableHead className="text-slate-400">Kubelet</TableHead>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-secondary)]">节点名称</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">IP</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">角色</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">配置</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">使用率</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">Pods</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">Kubelet</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...</TableCell></TableRow>
<TableRow><TableCell colSpan={8} className="text-center text-[var(--dg-text-muted)] py-12"><Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...</TableCell></TableRow>
) : filtered.length === 0 ? (
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Server className="h-8 w-8 mx-auto mb-2 opacity-50" />{selectedCluster ? '暂无节点数据' : '请选择集群'}</TableCell></TableRow>
<TableRow><TableCell colSpan={8} className="text-center text-[var(--dg-text-muted)] py-12"><Server className="h-8 w-8 mx-auto mb-2 opacity-50" />{selectedCluster ? '暂无节点数据' : '请选择集群'}</TableCell></TableRow>
) : (
filtered.map((n) => (
<TableRow key={n.id} className="border-slate-800 hover:bg-slate-800/30">
<TableCell className="font-medium text-slate-200">{n.name}</TableCell>
<TableCell><code className="text-sm text-slate-300 bg-slate-800 px-1.5 py-0.5 rounded">{n.ip || '-'}</code></TableCell>
<TableRow key={n.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{n.name}</TableCell>
<TableCell><code className="text-sm text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] px-1.5 py-0.5 rounded">{n.ip || '-'}</code></TableCell>
<TableCell>{getRoleBadge(n.role)}</TableCell>
<TableCell>{getStatusBadge(n.status)}</TableCell>
<TableCell className="text-sm text-slate-400">
<TableCell className="text-sm text-[var(--dg-text-secondary)]">
{n.cpu ? `${n.cpu}核` : '-'} / {n.memory ? `${Math.round(n.memory / 1024)}GB` : '-'}
</TableCell>
<TableCell>
{(() => {
const cpu = n.cpu_usage || 0;
const mem = n.memory_usage || 0;
const bar = (val: number) => val >= 85 ? 'bg-red-500' : val >= 60 ? 'bg-yellow-500' : 'bg-emerald-500';
const txt = (val: number) => val >= 85 ? 'text-red-400' : val >= 60 ? 'text-yellow-400' : 'text-emerald-400';
const bar = (val: number) => val >= 85 ? 'bg-[var(--dg-red)]' : val >= 60 ? 'bg-[var(--dg-orange)]' : 'bg-[var(--dg-green)]';
const txt = (val: number) => val >= 85 ? 'text-[var(--dg-red)]' : val >= 60 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-green)]';
return (
<div className="flex flex-col gap-1 min-w-[120px]">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-7">CPU</span>
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<span className="text-xs text-[var(--dg-text-muted)] w-7">CPU</span>
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)]/50 overflow-hidden">
<div className={`h-full rounded-full ${bar(cpu)}`} style={{ width: `${Math.min(cpu, 100)}%` }} />
</div>
<span className={`text-xs w-9 text-right ${txt(cpu)}`}>{cpu.toFixed(0)}%</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-7">Mem</span>
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<span className="text-xs text-[var(--dg-text-muted)] w-7">Mem</span>
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)]/50 overflow-hidden">
<div className={`h-full rounded-full ${bar(mem)}`} style={{ width: `${Math.min(mem, 100)}%` }} />
</div>
<span className={`text-xs w-9 text-right ${txt(mem)}`}>{mem.toFixed(0)}%</span>
@@ -155,8 +155,8 @@ export function KubernetesNodes() {
);
})()}
</TableCell>
<TableCell className="text-sm text-slate-400">{n.pod_count || 0} / {n.max_pods || '-'}</TableCell>
<TableCell className="text-xs text-slate-500">{n.kubelet_version || '-'}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{n.pod_count || 0} / {n.max_pods || '-'}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">{n.kubelet_version || '-'}</TableCell>
</TableRow>
))
)}