feat: 完善堡垒机与 Agent 智能运维能力

- 支持 Windows Agent 构建、安装脚本和统一上报协议

- 增强 RDP 剪贴板、虚拟盘和文件传输能力

- 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
Logan.Li committed 2026-07-05 23:49:36 +08:00
1 parent a9513c2056
commit e0d652d211
169 files changed
+12192 -9116

No files matched your search

+204 -137
View File
@@ -1,41 +1,44 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { CheckCircle, XCircle, Settings, Users, Shield } from 'lucide-react';
import { PageShell } from '@/components/layout/PageShell';
import { userApi } from '@/api';
import type { Role } from '@/types';
import { SystemUserTab } from './SystemUserTab';
import { SystemRolesTab } from './SystemRolesTab';
import { SystemSettingsTab } from './SystemSettingsTab';
import { ResetPasswordDialog } from './ResetPasswordDialog';
import { useLdapManagement } from './useLdapManagement';
import { useAuditSettings } from './useAuditSettings';
import type { UserFormData } from './UserFormDialog';
import { useTranslation } from 'react-i18next';
import { useState, useEffect } from "react";
import { useLocation } from "react-router-dom";
import { useConfirm } from "@/components/ui/confirm-dialog";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { CheckCircle, XCircle, Settings, Users, Shield } from "lucide-react";
import { PageShell } from "@/components/layout/PageShell";
import { userApi } from "@/api";
import type { Role } from "@/types";
import { SystemUserTab } from "./SystemUserTab";
import { SystemRolesTab } from "./SystemRolesTab";
import { SystemSettingsTab } from "./SystemSettingsTab";
import { ResetPasswordDialog } from "./ResetPasswordDialog";
import { useLdapManagement } from "./useLdapManagement";
import { useAuditSettings } from "./useAuditSettings";
import type { UserFormData } from "./UserFormDialog";
import { useTranslation } from "react-i18next";
// 后端用户数据转换为前端兼容格式
function mapUser( u: any) {
function mapUser(u: any) {
return {
...u,
status: u.status === 1 || u.status === 'active' ? ('active' as const) : ('inactive' as const),
status:
u.status === 1 || u.status === "active"
? ("active" as const)
: ("inactive" as const),
roles: Array.isArray(u.roles) ? u.roles : u.role ? [u.role] : [],
lastLoginAt: u.last_login ?? u.lastLoginAt,
createdAt: u.created_at ?? u.createdAt,
authSource: u.auth_source ?? u.authSource ?? 'local',
authSource: u.auth_source ?? u.authSource ?? "local",
};
}
const emptyForm: UserFormData = {
username: '',
nickname: '',
email: '',
phone: '',
role: '',
password: '',
username: "",
nickname: "",
email: "",
phone: "",
role: "",
password: "",
status: 1 as number,
};
@@ -45,32 +48,35 @@ export function SystemManager() {
const location = useLocation();
// 侧边栏菜单路径映射到 Tab // Logan.Li
// 侧边栏已有独立菜单项,按路由直接渲染对应页面,不再重复 Tab // Logan.Li
const pathToTab: Record<string, 'users' | 'roles' | 'settings'> = {
'/users': 'users',
'/roles': 'roles',
'/settings': 'settings',
const pathToTab: Record<string, "users" | "roles" | "settings"> = {
"/users": "users",
"/roles": "roles",
"/settings": "settings",
};
const activeTab = pathToTab[location.pathname] || 'users';
const activeTab = pathToTab[location.pathname] || "users";
const pageMeta: Record<string, { title: string; subtitle: string; icon: React.ReactNode }> = {
const pageMeta: Record<
string,
{ title: string; subtitle: string; icon: React.ReactNode }
> = {
users: {
title: t('system.users'),
subtitle: t('system.localUserDesc'),
title: t("system.users"),
subtitle: t("system.localUserDesc"),
icon: <Users className="h-5 w-5" />,
},
roles: {
title: t('system.roles'),
subtitle: t('system.roleBindingHint'),
title: t("system.roles"),
subtitle: t("system.roleBindingHint"),
icon: <Shield className="h-5 w-5" />,
},
settings: {
title: t('system.settings'),
subtitle: t('system.generalSubtitle'),
title: t("system.settings"),
subtitle: t("system.generalSubtitle"),
icon: <Settings className="h-5 w-5" />,
},
};
const meta = pageMeta[activeTab];
const [searchQuery, setSearchQuery] = useState('');
const [searchQuery, setSearchQuery] = useState("");
const [users, setUsers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<any | null>(null);
@@ -84,7 +90,9 @@ export function SystemManager() {
const [form, setForm] = useState<UserFormData>({ ...emptyForm });
// CSV 批量导入弹窗
const [importDialogOpen, setImportDialogOpen] = useState<'csv' | 'ldap' | null>(null);
const [importDialogOpen, setImportDialogOpen] = useState<
"csv" | "ldap" | null
>(null);
const [importPreview, setImportPreview] = useState<any[]>([]);
const [importResult, setImportResult] = useState<any>(null);
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
@@ -96,7 +104,7 @@ export function SystemManager() {
const rawUsers = res?.list ?? [];
setUsers(rawUsers.map(mapUser));
} catch (error) {
console.error('Failed to fetch users:', error);
console.error("Failed to fetch users:", error);
setUsers([]);
} finally {
setLoading(false);
@@ -114,28 +122,34 @@ export function SystemManager() {
// LDAP 导入 Dialog 打开时加载 OU 和用户
useEffect(() => {
if (importDialogOpen === 'ldap') {
if (importDialogOpen === "ldap") {
ldap.loadImportDialogData();
}
}, [importDialogOpen]);
const filteredUsers = users.filter(
(u) =>
(u.username || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.nickname || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.email || '').toLowerCase().includes(searchQuery.toLowerCase())
(u.username || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.nickname || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.email || "").toLowerCase().includes(searchQuery.toLowerCase()),
);
const getUserStatusBadge = (status: 'active' | 'inactive') => {
return status === 'active' ? (
<Badge variant="outline" className="border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10">
const getUserStatusBadge = (status: "active" | "inactive") => {
return status === "active" ? (
<Badge
variant="outline"
className="border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10"
>
<CheckCircle className="h-3 w-3 mr-1" />
{t('common.normal')}
{t("common.normal")}
</Badge>
) : (
<Badge variant="outline" className="border-[var(--dg-text-muted)]/30 text-[var(--dg-text-muted)] bg-[var(--dg-text-muted)]/10">
<Badge
variant="outline"
className="border-[var(--dg-text-muted)]/30 text-[var(--dg-text-muted)] bg-[var(--dg-text-muted)]/10"
>
<XCircle className="h-3 w-3 mr-1" />
{t('system.userInactive')}
{t("system.userInactive")}
</Badge>
);
};
@@ -146,16 +160,16 @@ export function SystemManager() {
setDialogOpen(true);
};
const openEdit = ( user: any) => {
const openEdit = (user: any) => {
setEditingUser(user);
setForm({
username: user.username || '',
nickname: user.nickname || '',
email: user.email || '',
phone: user.phone || '',
role: Array.isArray(user.roles) ? user.roles[0] || '' : user.role || '',
password: '',
status: user.status === 'active' ? 1 : 0,
username: user.username || "",
nickname: user.nickname || "",
email: user.email || "",
phone: user.phone || "",
role: Array.isArray(user.roles) ? user.roles[0] || "" : user.role || "",
password: "",
status: user.status === "active" ? 1 : 0,
});
setDialogOpen(true);
};
@@ -182,41 +196,52 @@ export function SystemManager() {
setDialogOpen(false);
await loadUsers();
} catch (error) {
console.error('Failed to save user:', error);
console.error("Failed to save user:", error);
} finally {
setSaving(false);
}
};
const handleDelete = async ( user: any) => {
const ok = await confirm({ title: t('system.deleteUser'), description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: t('common.delete'), variant: 'destructive' });
const handleDelete = async (user: any) => {
const ok = await confirm({
title: t("system.deleteUser"),
description: `确认删除用户「${user.nickname || user.username}」吗?`,
confirmText: t("common.delete"),
variant: "destructive",
});
if (!ok) return;
try {
await userApi.deleteUser(user.id);
setSelectedUserIds((prev) => prev.filter((id) => id !== user.id));
await loadUsers();
toast.success(t('system.userDeleted'));
toast.success(t("system.userDeleted"));
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : t('common.deleteFailed'));
toast.error(
error instanceof Error ? error.message : t("common.deleteFailed"),
);
}
};
const handleBatchDelete = async () => {
if (selectedUserIds.length === 0) return;
const ok = await confirm({
title: t('system.batchDeleteUsers'),
title: t("system.batchDeleteUsers"),
description: `确定删除选中的 ${selectedUserIds.length} 个用户吗?此操作不可恢复。`,
confirmText: t('system.terminalBatchDelete'),
variant: 'destructive',
confirmText: t("system.terminalBatchDelete"),
variant: "destructive",
});
if (!ok) return;
try {
const res = await userApi.batchDelete(selectedUserIds) as { message?: string };
const res = (await userApi.batchDelete(selectedUserIds)) as {
message?: string;
};
toast.success(res?.message || `已删除 ${selectedUserIds.length} 个用户`);
setSelectedUserIds([]);
await loadUsers();
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : t('cloud.batchDeleteFailed'));
toast.error(
error instanceof Error ? error.message : t("cloud.batchDeleteFailed"),
);
}
};
@@ -227,44 +252,64 @@ export function SystemManager() {
const reader = new FileReader();
reader.onload = (ev) => {
const text = ev.target?.result as string;
const lines = text.split('\n').filter((l) => l.trim() && !l.startsWith('\xEF\xBB\xBF'));
const lines = text
.split("\n")
.filter((l) => l.trim() && !l.startsWith("\xEF\xBB\xBF"));
if (lines.length < 2) return;
// Find column indices from header
const headers = lines[0].split(',').map((h) => h.trim().toLowerCase());
const idxUsername = headers.findIndex((h) => h.includes('用户名') || h.includes('username'));
const idxNickname = headers.findIndex((h) => h.includes('昵称') || h.includes('name') || h.includes('显示名'));
const idxEmail = headers.findIndex((h) => h.includes('邮箱') || h.includes('email') || h.includes('mail'));
const idxPhone = headers.findIndex((h) => h.includes('手机') || h.includes('phone'));
const idxRole = headers.findIndex((h) => h.includes('角色') || h.includes('role'));
const idxPassword = headers.findIndex((h) => h.includes('密码') || h.includes('password'));
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
const idxUsername = headers.findIndex(
(h) => h.includes("用户名") || h.includes("username"),
);
const idxNickname = headers.findIndex(
(h) => h.includes("昵称") || h.includes("name") || h.includes("显示名"),
);
const idxEmail = headers.findIndex(
(h) => h.includes("邮箱") || h.includes("email") || h.includes("mail"),
);
const idxPhone = headers.findIndex(
(h) => h.includes("手机") || h.includes("phone"),
);
const idxRole = headers.findIndex(
(h) => h.includes("角色") || h.includes("role"),
);
const idxPassword = headers.findIndex(
(h) => h.includes("密码") || h.includes("password"),
);
const parsed: Record<string, unknown>[] = [];
for (let i = 1; i < lines.length; i++) {
const cols = lines[i].split(',').map((c) => c.trim());
const cols = lines[i].split(",").map((c) => c.trim());
parsed.push({
username: idxUsername >= 0 ? cols[idxUsername] : '',
nickname: idxNickname >= 0 ? cols[idxNickname] : '',
email: idxEmail >= 0 ? cols[idxEmail] : '',
phone: idxPhone >= 0 ? cols[idxPhone] : '',
role: idxRole >= 0 ? cols[idxRole] : '',
password: idxPassword >= 0 ? cols[idxPassword] : '',
username: idxUsername >= 0 ? cols[idxUsername] : "",
nickname: idxNickname >= 0 ? cols[idxNickname] : "",
email: idxEmail >= 0 ? cols[idxEmail] : "",
phone: idxPhone >= 0 ? cols[idxPhone] : "",
role: idxRole >= 0 ? cols[idxRole] : "",
password: idxPassword >= 0 ? cols[idxPassword] : "",
});
}
setImportPreview(parsed);
setImportResult(null);
};
reader.readAsText(file, 'UTF-8');
reader.readAsText(file, "UTF-8");
};
// 执行 CSV 批量导入
const handleBatchImport = async () => {
ldap.setImporting(true);
try {
const res = await userApi.batchImport({ users: importPreview, skip_existing: true });
const res = await userApi.batchImport({
users: importPreview,
skip_existing: true,
});
setImportResult(res);
await loadUsers();
} catch (error) {
setImportResult({ code: 500, message: error?.message || t('system.importFailed') });
setImportResult({
code: 500,
message: error?.message || t("system.importFailed"),
});
} finally {
ldap.setImporting(false);
}
@@ -272,74 +317,93 @@ export function SystemManager() {
return (
<PageShell
variant={activeTab === 'settings' ? 'config' : 'list'}
maxWidth={activeTab === 'settings' ? 'xl' : 'full'}
variant={activeTab === "settings" ? "config" : "list"}
maxWidth={activeTab === "settings" ? "xl" : "full"}
title={meta.title}
subtitle={meta.subtitle}
icon={meta.icon}
>
<div className="w-full">
{activeTab === 'users' && (
<div className="space-y-4">
<SystemUserTab
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
filteredUsers={filteredUsers}
loading={loading}
selectedUser={selectedUser}
setSelectedUser={setSelectedUser}
getUserStatusBadge={getUserStatusBadge}
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
editingUser={editingUser}
form={form}
setForm={setForm}
saving={saving}
handleSubmit={handleSubmit}
openCreate={openCreate}
openEdit={openEdit}
handleDelete={handleDelete}
handleBatchDelete={handleBatchDelete}
selectedUserIds={selectedUserIds}
setSelectedUserIds={setSelectedUserIds}
onResetPassword={(u) => { setSelectedUser(null); setResetPasswordUser(u); }}
importDialogOpen={importDialogOpen}
setImportDialogOpen={setImportDialogOpen}
importPreview={importPreview}
setImportPreview={setImportPreview}
importing={ldap.importing}
importResult={importResult}
setImportResult={setImportResult}
handleCsvUpload={handleCsvUpload}
handleBatchImport={handleBatchImport}
ldapUsers={ldap.ldapUsers}
ldapSelected={ldap.ldapSelected}
setLdapSelected={ldap.setLdapSelected}
ldapLoading={ldap.ldapLoading}
ldapImportOUs={ldap.ldapImportOUs}
ldapImportOUDN={ldap.ldapImportOUDN}
setLdapImportOUDN={ldap.setLdapImportOUDN}
loadLdapUsers={ldap.loadLdapUsers}
handleLdapImport={() => ldap.handleLdapImport({ setResult: setImportResult })}
setLdapUsers={ldap.setLdapUsers}
/>
</div>
{activeTab === "users" && (
<div className="space-y-4">
<SystemUserTab
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
filteredUsers={filteredUsers}
loading={loading}
selectedUser={selectedUser}
setSelectedUser={setSelectedUser}
getUserStatusBadge={getUserStatusBadge}
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
editingUser={editingUser}
form={form}
setForm={setForm}
saving={saving}
handleSubmit={handleSubmit}
openCreate={openCreate}
openEdit={openEdit}
handleDelete={handleDelete}
handleBatchDelete={handleBatchDelete}
selectedUserIds={selectedUserIds}
setSelectedUserIds={setSelectedUserIds}
onResetPassword={(u) => {
setSelectedUser(null);
setResetPasswordUser(u);
}}
importDialogOpen={importDialogOpen}
setImportDialogOpen={setImportDialogOpen}
importPreview={importPreview}
setImportPreview={setImportPreview}
importing={ldap.importing}
importResult={importResult}
setImportResult={setImportResult}
handleCsvUpload={handleCsvUpload}
handleBatchImport={handleBatchImport}
ldapUsers={ldap.ldapUsers}
ldapSelected={ldap.ldapSelected}
setLdapSelected={ldap.setLdapSelected}
ldapLoading={ldap.ldapLoading}
ldapImportOUs={ldap.ldapImportOUs}
ldapImportOUDN={ldap.ldapImportOUDN}
setLdapImportOUDN={ldap.setLdapImportOUDN}
loadLdapUsers={ldap.loadLdapUsers}
handleLdapImport={() =>
ldap.handleLdapImport({ setResult: setImportResult })
}
setLdapUsers={ldap.setLdapUsers}
/>
</div>
)}
{activeTab === 'roles' && (
{activeTab === "roles" && (
<SystemRolesTab
selectedRole={selectedRole}
setSelectedRole={setSelectedRole}
/>
)}
{activeTab === 'settings' && (
{activeTab === "settings" && (
<SystemSettingsTab
{...ldap.settingsTabProps}
auditRetentionDays={audit.retentionDays}
setAuditRetentionDays={audit.setRetentionDays}
auditRecordAll={audit.recordAll}
setAuditRecordAll={audit.setRecordAll}
sessionWatermarkEnabled={audit.sessionWatermarkEnabled}
setSessionWatermarkEnabled={audit.setSessionWatermarkEnabled}
sessionRecordEnabled={audit.sessionRecordEnabled}
setSessionRecordEnabled={audit.setSessionRecordEnabled}
clipboardEnabled={audit.clipboardEnabled}
setClipboardEnabled={audit.setClipboardEnabled}
fileUploadEnabled={audit.fileUploadEnabled}
setFileUploadEnabled={audit.setFileUploadEnabled}
fileDownloadEnabled={audit.fileDownloadEnabled}
setFileDownloadEnabled={audit.setFileDownloadEnabled}
commandPolicyEnabled={audit.commandPolicyEnabled}
setCommandPolicyEnabled={audit.setCommandPolicyEnabled}
approvalRequired={audit.approvalRequired}
setApprovalRequired={audit.setApprovalRequired}
auditLoading={audit.loading}
auditApplying={audit.applying}
handleAuditApply={audit.handleApply}
@@ -349,7 +413,10 @@ export function SystemManager() {
)}
</div>
<ResetPasswordDialog user={resetPasswordUser} onClose={() => setResetPasswordUser(null)} />
<ResetPasswordDialog
user={resetPasswordUser}
onClose={() => setResetPasswordUser(null)}
/>
</PageShell>
);
}