import { AuditAction, type AuditEvent, CAPABILITY_CATALOG, type KnownCapabilityId } from "@umt/contracts"; import { X } from "lucide-react"; import { useEffect, useState } from "react"; import { useSearchParams } from "react-router"; import { useAuditEvents } from "~/api/audit.ts"; import { DataTable } from "~/components/console/data-table.tsx"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { PageHeader } from "~/components/console/page-header.tsx"; import { Button } from "~/components/ui/button.tsx"; import { ACTOR_TYPES, actorTypeLabel, formatRelative } from "~/lib/labels.ts"; import { formatDateTime } from "~/lib/utils.ts"; export const meta = () => [{ title: "审计 · UHD 媒体自动化控制台" }]; const actorLabel = actorTypeLabel; const ACTION_LABEL: Record = { "admin.setup": "初始化管理员", "admin.login_success": "登录", "admin.logout": "退出登录", "admin.password_change": "修改密码", "admin.totp_enable": "启用二次验证", "admin.totp_disable": "停用二次验证", "api_key.create": "创建 API Key", "api_key.update": "修改 API Key", "api_key.revoke": "撤销 API Key", "settings.update": "修改设置", "secret.create": "新建凭证", "secret.rotate": "轮换凭证", "secret.delete": "删除凭证", "site_account.credentials_set": "录入站点凭证", "engine.run_cancel": "取消任务", "pi_upstream.oauth_start": "开始订阅登录", "pi_upstream.oauth_login": "完成订阅登录", "pi_upstream.oauth_logout": "退出订阅登录", "schedule.run_now": "立即执行定时任务", "schedule.skip": "跳过定时任务", "acquisition_plan.manual_submit": "手动提交下载", "acquisition_plan.cancel": "取消下载", "engine.run_retry": "重跑任务", "capability.invoke": "智能体操作", "capability.denied": "智能体操作被拒绝", "site.created": "登记站点", "site.updated": "修改站点", "feed_source.created": "新建 RSS", "identity.release_assigned": "指定资源身份", "schedule.update": "修改定时任务", "files.mkdir": "新建目录", "files.rename": "重命名", "files.move": "移动文件", "files.copy": "复制文件", "files.organize": "整理文件", "files.delete": "删除文件", "files.trash_restore": "从回收站还原", }; const VERB: Record = { create: "新建", update: "修改", delete: "删除", test: "测试", verify: "校验" }; const NOUN: Record = { library_root: "媒体库目录", downloader_instance: "下载器", media_server_instance: "媒体服务器", path_mapping: "路径映射", pi_upstream: "模型服务", schedule: "定时任务", automation_grant: "持续授权", }; const TARGET_LABEL: Record = { ...NOUN, admin_user: "管理员", api_key: "API Key", settings: "设置", secret: "凭证", site: "站点", site_account: "站点账号", feed_source: "RSS", engine_run: "任务", acquisition_plan: "下载", download_task: "下载任务", media_entity: "作品", source_release: "资源", organization_record: "整理记录", file: "文件", capability: "能力", }; /** 对象列:能力显示中文名,其余显示对象类型;ID 只在详情里 */ function targetText(type: string, id: string | null): string { if (type === "capability" && id) return CAPABILITY_CATALOG[id as KnownCapabilityId]?.title ?? id; return TARGET_LABEL[type] ?? type; } function actionLabel(a: string): string { if (ACTION_LABEL[a]) return ACTION_LABEL[a]; const [noun, verb] = a.split("."); if (noun && verb && NOUN[noun] && VERB[verb]) return `${VERB[verb]}${NOUN[noun]}`; return a; } // 前缀组 + 具体动作 const ACTION_OPTIONS = [ ...[ "admin", "api_key", "settings", "secret", "library_root", "downloader_instance", "media_server_instance", "path_mapping", ].map((p) => ({ value: `${p}.*`, label: `${p}.*(全部)` })), ...AuditAction.options.map((a) => ({ value: a, label: `${actionLabel(a)} · ${a}` })), ]; export default function AuditPage() { const [params, setParams] = useSearchParams(); const actorType = params.get("actorType") ?? ""; const action = params.get("action") ?? ""; const targetId = params.get("targetId") ?? ""; const [target, setTarget] = useState(params.get("targetType") ?? ""); const [targetType, setTargetType] = useState(target); const [selected, setSelected] = useState(null); useEffect(() => { const t = setTimeout(() => { const v = target.trim(); setTargetType(v); setParams( (p) => { if (v) p.set("targetType", v); else p.delete("targetType"); return p; }, { replace: true }, ); }, 350); return () => clearTimeout(t); }, [target, setParams]); const setParam = (k: string, v: string) => setParams( (p) => { if (v) p.set(k, v); else p.delete(k); return p; }, { replace: true }, ); const q = useAuditEvents({ actorType: actorType || undefined, action: action || undefined, targetType: targetType || undefined, targetId: targetId || undefined, }); const rows = q.data?.pages.flatMap((p) => p.items); const filtered = !!(actorType || action || targetType || targetId); return ( <> { setTarget(""); setParams({}, { replace: true }); } : undefined } > setParam("actorType", v)} options={ACTOR_TYPES} /> setParam("action", v)} options={ACTION_OPTIONS} /> {targetId ? ( ) : null} paginationKey={JSON.stringify({ actorType, action, targetType, targetId })} rows={rows} rowKey={(r) => r.id} loading={q.isPending} error={q.error} onRetry={() => q.refetch()} empty={{ title: filtered ? "没有符合条件的审计事件" : "还没有审计事件", }} onRowClick={setSelected} selectedKey={selected?.id ?? null} hasMore={!!q.hasNextPage} onLoadMore={() => q.fetchNextPage()} loadingMore={q.isFetchingNextPage} columns={[ { key: "at", header: "时间", cell: (r) => ( {formatRelative(r.at)} ), }, { key: "action", header: "动作", cell: (r) => (

{actionLabel(r.action)}

), }, { key: "actor", header: "操作者", hideBelow: "sm", cell: (r) => actorLabel(r.actorType) }, { key: "target", header: "对象", hideBelow: "md", cell: (r) => r.targetType ? (

{targetText(r.targetType, r.targetId)}

) : ( "—" ), }, ]} /> !o && setSelected(null)} title={selected ? actionLabel(selected.action) : ""} description={selected ? formatDateTime(selected.at) : undefined} > {selected ? ( <> ) : null} ); }