feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)

- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
attacker committed 2026-06-26 21:13:15 +08:00
1 parent 5ca0e6d83a
commit c68aef4444
150 files changed
+1038 -690

No files matched your search

@@ -44,6 +44,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { toast } from 'sonner';
import { pipelineApi, deployApi } from '@/api';
import { useConfirm } from '@/components/ui/confirm-dialog';
import type { JsonData } from '@/types/common';
// ---- types ----
interface Task {
@@ -58,7 +59,7 @@ interface LogEntry {
}
const stageTypeLabels: Record<string, string> = { source: '拉取代码', build: '构建', deploy: '部署', test: '测试', custom: '自定义' };
const taskIcons: Record<string, any> = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
const taskIcons: JsonData = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
function uid() { return `tsk-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; }
function fmtDuration(s: number | undefined): string {
@@ -119,7 +120,7 @@ function TaskEditor({ task, onUpdate }: { task: Task | null; onUpdate: (t: Task)
}
// ====== Log Dialog ======
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run: any; logs: LogEntry[] }) {
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run?: JsonData; logs: LogEntry[] }) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
@@ -160,16 +161,16 @@ function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () =>
export default function PipelineEditor() {
const { code } = useParams<{ code: string }>();
const pipelineCode = code || '';
const [pipeline, setPipeline] = useState<any>(null);
const [pipeline, setPipeline] = useState<JsonData>(null);
const [stages, setStages] = useState<Stage[]>([]);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const [expandedStages, setExpandedStages] = useState<Record<string, boolean>>({});
const [activeTab, setActiveTab] = useState('flow');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [runs, setRuns] = useState<any[]>([]);
const [environments, setEnvironments] = useState<any[]>([]);
const [members, setMembers] = useState<any[]>([]);
const [runs, setRuns] = useState<JsonData[]>([]);
const [environments, setEnvironments] = useState<JsonData[]>([]);
const [members, setMembers] = useState<JsonData[]>([]);
const [loadingMembers, setLoadingMembers] = useState(false);
// trigger config
@@ -186,7 +187,7 @@ export default function PipelineEditor() {
// log dialog
const [logOpen, setLogOpen] = useState(false);
const [logRun, setLogRun] = useState<any>(null);
const [logRun, setLogRun] = useState<JsonData>(null);
const [logs, setLogs] = useState<LogEntry[]>([]);
const _confirm = useConfirm();
@@ -194,7 +195,7 @@ export default function PipelineEditor() {
const loadEnvironments = useCallback(async () => {
try {
const res = await deployApi.getEnvironments();
setEnvironments(Array.isArray(res) ? res : (res as Record<string, any>)?.list || []);
setEnvironments(Array.isArray(res) ? res : (res as JsonData)?.list || []);
} catch { /* silent */ }
}, []);
@@ -202,7 +203,7 @@ export default function PipelineEditor() {
setLoadingMembers(true);
try {
const res = await pipelineApi.getMembers(pipelineCode);
setMembers(Array.isArray(res) ? res : (res as Record<string, any>)?.data || []);
setMembers(Array.isArray(res) ? res : (res as JsonData)?.data || []);
} catch { setMembers([]); }
finally { setLoadingMembers(false); }
}, [pipelineCode]);
@@ -210,7 +211,7 @@ export default function PipelineEditor() {
const loadRuns = useCallback(async () => {
try {
const res = await pipelineApi.getRuns(pipelineCode);
setRuns(Array.isArray(res) ? res : (res as Record<string, any>)?.list || []);
setRuns(Array.isArray(res) ? res : (res as JsonData)?.list || []);
} catch { setRuns([]); }
}, [pipelineCode]);
@@ -218,7 +219,7 @@ export default function PipelineEditor() {
(async () => {
setLoading(true);
try {
const p = await pipelineApi.getPipelineDetail(pipelineCode) as Record<string, any>;
const p = await pipelineApi.getPipelineDetail(pipelineCode) as JsonData;
// http.get 已自动解包 json.data,p 即为 pipeline 对象
if (!p || !p.id) { setPipeline(null); setLoading(false); return; }
setPipeline(p);
@@ -294,11 +295,11 @@ export default function PipelineEditor() {
} catch { toast.error('停止失败'); }
};
const handleViewLog = async ( run: any) => {
const handleViewLog = async (run: JsonData) => {
setLogRun(run);
setLogOpen(true);
try {
const data = await pipelineApi.getRunLog(pipelineCode, run.id) as Record<string, any>;
const data = await pipelineApi.getRunLog(pipelineCode, run.id) as JsonData;
setLogs(data?.logs || []);
} catch { setLogs([]); }
};
@@ -482,7 +483,7 @@ export default function PipelineEditor() {
</tr>
</thead>
<tbody>
{runs.map(( r: any) => (
{runs.map((r: JsonData) => (
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">#{r.build_number || r.id}</td>
<td className="px-3 py-2">
@@ -552,8 +553,8 @@ export default function PipelineEditor() {
{/* Members list — split direct users vs group assignments */}
{(() => {
const directUsers = members.filter(( m: any) => m.username);
const groupAssignments = members.filter(( m: any) => m.group_id);
const directUsers = members.filter((m: JsonData) => m.username);
const groupAssignments = members.filter((m: JsonData) => m.group_id);
return (
<>
{/* Direct users */}
@@ -568,7 +569,7 @@ export default function PipelineEditor() {
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">暂无直接用户</div>
) : (
<div className="divide-y divide-[var(--dg-border)]">
{directUsers.map(( m: any) => (
{directUsers.map((m: JsonData) => (
<div key={m.id} className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center"><Users className="w-4 h-4 text-blue-400" /></div>
@@ -597,7 +598,7 @@ export default function PipelineEditor() {
</div>
) : (
<div className="divide-y divide-[var(--dg-border)]">
{groupAssignments.map(( m: any) => (
{groupAssignments.map((m: JsonData) => (
<div key={m.id} className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center"><Shield className="w-4 h-4 text-purple-400" /></div>