From 11c97cd885bc103f2f4d2cda5fb7f8f844156980 Mon Sep 17 00:00:00 2001 From: attacker Date: Tue, 16 Jun 2026 06:05:44 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86AIPredictiveAnaly?= =?UTF-8?q?tics.tsx(670=E2=86=92417=E8=A1=8C)=E6=8F=90=E5=8F=96predictUtil?= =?UTF-8?q?s=E5=AD=90=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiops/AIPredictiveAnalytics.tsx | 289 +--------------- .../aiops/PredictiveCapacityTab.tsx | 2 +- frontend/src/components/aiops/predictUtils.ts | 320 ++++++++++++++++++ 3 files changed, 339 insertions(+), 272 deletions(-) create mode 100644 frontend/src/components/aiops/predictUtils.ts diff --git a/frontend/src/components/aiops/AIPredictiveAnalytics.tsx b/frontend/src/components/aiops/AIPredictiveAnalytics.tsx index 5218f80..c9ea0df 100644 --- a/frontend/src/components/aiops/AIPredictiveAnalytics.tsx +++ b/frontend/src/components/aiops/AIPredictiveAnalytics.tsx @@ -20,143 +20,25 @@ import { Cpu, MemoryStick, Network, - Database, Activity, BarChart3, LineChart, Calendar, Sparkles, Zap, - ArrowUpRight, Minus, Loader2, } from 'lucide-react'; import { dashboardStatsApi, serverApi, alertApi } from '@/services/api'; import { PredictiveCapacityTab } from './PredictiveCapacityTab'; +import { + computePredictions, + trendColorClass, + type CapacityForecast, +} from './predictUtils'; -interface PredictionMetric { - name: string; - current: number; - predicted: number; - unit: string; - trend: 'up' | 'down' | 'stable'; - confidence: number; - timeRange: string; -} - -export interface CapacityForecast { - resource: string; - metricKey: string; - currentUsage: number; - predictedUsage: number; - threshold: number; - daysUntilThreshold: number; - recommendation: string; -} - -interface AnomalyPrediction { - metric: string; - predictedAnomaly: string; - probability: number; - timeframe: string; - suggestedAction: string; -} - -/** 指标定义:后端 metric name → 展示标签 / 告警表达式前缀 / 阈值 */ -const METRIC_DEFS = [ - { key: 'cpu', label: 'CPU使用率', exprKey: 'cpu_usage', threshold: 85 }, - { key: 'memory', label: '内存使用率', exprKey: 'memory_usage', threshold: 85 }, - { key: 'disk', label: '磁盘使用率', exprKey: 'disk_usage', threshold: 90 }, -] as const; - -const TIME_RANGE_LABELS: Record = { - '1h': '1小时后', - '24h': '24小时后', - '7d': '7天后', - '30d': '30天后', -}; - -const TIME_RANGE_HOURS: Record = { - '1h': 1, - '24h': 24, - '7d': 168, - '30d': 720, -}; - -/** 简单最小二乘线性回归,返回斜率和 R² */ -function linearRegression(values: number[]): { slope: number; r2: number } { - const n = values.length; - if (n < 2) return { slope: 0, r2: 0 }; - const xs = values.map((_, i) => i); - const meanX = xs.reduce((a, b) => a + b, 0) / n; - const meanY = values.reduce((a, b) => a + b, 0) / n; - let num = 0, - den = 0, - ssTot = 0, - ssRes = 0; - for (let i = 0; i < n; i++) { - num += (xs[i] - meanX) * (values[i] - meanY); - den += (xs[i] - meanX) ** 2; - ssTot += (values[i] - meanY) ** 2; - } - const slope = den === 0 ? 0 : num / den; - const intercept = meanY - slope * meanX; - for (let i = 0; i < n; i++) { - const pred = intercept + slope * xs[i]; - ssRes += (values[i] - pred) ** 2; - } - const r2 = ssTot === 0 ? 1 : Math.max(0, 1 - ssRes / ssTot); - return { slope, r2 }; -} - -/** 从时序指标中提取按时间排序的数值序列 */ -function extractSeries(metrics: any[], name: string): number[] { - return metrics - .filter((m) => m.name === name) - .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) - .map((m) => Number(m.value) || 0); -} - -/** 从告警表达式中解析阈值,如 "cpu_usage > 80" → 80 */ -function parseThreshold(expr: string): number | null { - const m = expr.match(/[><=]\s*([\d.]+)/); - return m ? parseFloat(m[1]) : null; -} - -/** 容量规划建议 */ -function capacityRecommendation(metricKey: string, days: number): string { - const level = days <= 7 ? 'urgent' : days <= 14 ? 'warning' : 'normal'; - const recs: Record> = { - urgent: { - cpu: 'CPU增长趋势明显,建议尽快扩容计算资源或优化高负载进程', - memory: '内存增长趋势明显,建议增加内存或排查内存泄漏', - disk: '磁盘空间即将耗尽,建议立即扩容或清理历史数据与日志', - }, - warning: { - cpu: 'CPU使用率呈上升趋势,建议持续监控并准备扩容计划', - memory: '内存使用率呈上升趋势,建议优化应用内存使用', - disk: '磁盘使用率持续增长,建议规划数据归档和清理策略', - }, - normal: { - cpu: 'CPU使用正常,建议持续监控', - memory: '内存使用正常,建议持续监控', - disk: '磁盘空间充足,建议持续监控', - }, - }; - return recs[level]?.[metricKey] || '建议持续监控资源使用趋势'; -} - -/** 异常建议操作 */ -function anomalyAction(metricKey: string): string { - const actions: Record = { - cpu: '检查高负载进程,优化CPU密集型任务,考虑横向扩容', - memory: '排查内存泄漏,优化进程内存配置,考虑增加节点', - disk: '清理历史数据和日志,实施数据归档策略,扩容磁盘', - service: '检查服务健康状态与依赖组件,查看服务日志', - pod: '检查Pod重启原因,查看容器日志,排查OOM或健康检查失败', - }; - return actions[metricKey] || '建议检查相关服务状态并持续监控'; -} +// 重新导出 CapacityForecast,兼容已有 import 路径 './AIPredictiveAnalytics' +export type { CapacityForecast }; export function AIPredictiveAnalytics() { const [selectedTimeRange, setSelectedTimeRange] = useState('24h'); @@ -201,139 +83,15 @@ export function AIPredictiveAnalytics() { setIsAnalyzing(false); }; - // ============ 计算指标预测 ============ - const horizonHours = TIME_RANGE_HOURS[selectedTimeRange] || 24; - const timeRangeLabel = TIME_RANGE_LABELS[selectedTimeRange] || '24小时后'; - - const predictions: PredictionMetric[] = METRIC_DEFS.map((def) => { - const series = extractSeries(rawMetrics, def.key); - const { slope, r2 } = linearRegression(series); - const avgFallback = - def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk; - const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0; - let predicted = current + slope * horizonHours; - predicted = Math.max(0, Math.min(100, predicted)); - const trend: 'up' | 'down' | 'stable' = slope > 0.05 ? 'up' : slope < -0.05 ? 'down' : 'stable'; - const confidence = Math.round(60 + r2 * 35); - return { - name: def.label, - current: Math.round(current), - predicted: Math.round(predicted), - unit: '%', - trend, - confidence, - timeRange: timeRangeLabel, - }; - }); - - // ============ 计算容量规划 ============ - const capacityForecasts: CapacityForecast[] = METRIC_DEFS.map((def) => { - const series = extractSeries(rawMetrics, def.key); - const { slope } = linearRegression(series); - const avgFallback = - def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk; - const currentUsage = Math.round(series.length > 0 ? series[series.length - 1] : avgFallback || 0); - const slopePerDay = slope * 24; - const predictedUsage = Math.round(Math.min(100, currentUsage + slopePerDay * 7)); - const threshold = def.threshold; - let daysUntilThreshold = 999; - if (slopePerDay > 0.01) { - daysUntilThreshold = Math.round((threshold - currentUsage) / slopePerDay); - daysUntilThreshold = daysUntilThreshold < 1 ? 1 : daysUntilThreshold; - } - const resourceNames: Record = { - cpu: '服务器CPU算力', - memory: '服务器内存', - disk: '存储磁盘空间', - }; - return { - resource: resourceNames[def.key] || def.label, - metricKey: def.key, - currentUsage, - predictedUsage, - threshold, - daysUntilThreshold, - recommendation: capacityRecommendation(def.key, daysUntilThreshold), - }; - }).filter((f) => f.currentUsage > 0); - - // ============ 计算异常预测 ============ - const anomalyPredictions: AnomalyPrediction[] = []; - - // 基于 metric 告警规则 + 当前趋势计算异常概率 - for (const rule of alertRules) { - if (!rule.enabled) continue; - const threshold = parseThreshold(rule.expression || ''); - const exprLower = (rule.expression || '').toLowerCase(); - const matchedDef = METRIC_DEFS.find((def) => exprLower.includes(def.exprKey)); - - if (matchedDef && threshold) { - const series = extractSeries(rawMetrics, matchedDef.key); - const { slope } = linearRegression(series); - const avgFallback = - matchedDef.key === 'cpu' - ? serverStats.avg_cpu - : matchedDef.key === 'memory' - ? serverStats.avg_memory - : serverStats.avg_disk; - const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0; - const ratio = threshold > 0 ? current / threshold : 0; - let probability = Math.round(ratio * 55); - if (slope > 0.05) probability += 15; - const eventCount = alertEvents.filter( - (a) => a.rule_id === rule.id || a.rule_name === rule.name, - ).length; - probability += Math.min(20, eventCount * 3); - probability = Math.max(5, Math.min(90, probability)); - - // 时间范围:基于斜率估算何时触及阈值 - let timeframe = '未来24小时'; - if (slope > 0.01 && threshold > current) { - const hours = (threshold - current) / slope; - if (hours < 4) timeframe = '未来4小时'; - else if (hours < 12) timeframe = '未来12小时'; - else if (hours < 24) timeframe = '未来24小时'; - else timeframe = `未来${Math.ceil(hours / 24)}天`; - } - - anomalyPredictions.push({ - metric: rule.name, - predictedAnomaly: rule.description || `${rule.name}可能触发`, - probability, - timeframe, - suggestedAction: anomalyAction(matchedDef.key), - }); - } else { - // 非 metric 规则(如服务不可用、Pod重启)基于事件频率 - const eventCount = alertEvents.filter( - (a) => a.rule_id === rule.id || a.rule_name === rule.name, - ).length; - if (eventCount > 0) { - const exprKey = exprLower.includes('service') ? 'service' : exprLower.includes('pod') ? 'pod' : 'service'; - anomalyPredictions.push({ - metric: rule.name, - predictedAnomaly: rule.description || `${rule.name}可能再次发生`, - probability: Math.min(60, 20 + eventCount * 5), - timeframe: '未来24小时', - suggestedAction: anomalyAction(exprKey), - }); - } - } - } - - // 去重:同一指标名只保留概率最高的一条 - const dedupedAnomalies = anomalyPredictions - .sort((a, b) => b.probability - a.probability) - .filter((item, idx, arr) => arr.findIndex((x) => x.metric === item.metric) === idx); - - // ============ 概览卡片数据 ============ - const avgR2 = METRIC_DEFS.length > 0 - ? METRIC_DEFS.reduce((sum, def) => sum + linearRegression(extractSeries(rawMetrics, def.key)).r2, 0) / - METRIC_DEFS.length - : 0; - const accuracy = Math.round(78 + avgR2 * 17); - const predictedAlertCount = dedupedAnomalies.filter((a) => a.probability >= 50).length; - const capacityWarningCount = capacityForecasts.filter((f) => f.daysUntilThreshold <= 14).length; + // ============ 综合预测推算(线性回归 + 告警规则阈值 + 事件频率) ============ + const { + predictions, + capacityForecasts, + dedupedAnomalies, + accuracy, + predictedAlertCount, + capacityWarningCount, + } = computePredictions({ rawMetrics, serverStats, alertEvents, alertRules, selectedTimeRange }); const getTrendIcon = (trend: string) => { switch (trend) { @@ -346,17 +104,6 @@ export function AIPredictiveAnalytics() { } }; - const getTrendColor = (trend: string) => { - switch (trend) { - case 'up': - return 'text-red-400'; - case 'down': - return 'text-emerald-400'; - default: - return 'text-slate-400'; - } - }; - return (
{/* Header */} @@ -511,7 +258,7 @@ export function AIPredictiveAnalytics() {
{getTrendIcon(prediction.trend)} - + {prediction.trend === 'up' ? '上升' : prediction.trend === 'down' ? '下降' : '平稳'}
@@ -527,7 +274,7 @@ export function AIPredictiveAnalytics() {

预测值 ({prediction.timeRange})

-

+

{prediction.predicted} {prediction.unit}

diff --git a/frontend/src/components/aiops/PredictiveCapacityTab.tsx b/frontend/src/components/aiops/PredictiveCapacityTab.tsx index 1435af2..1098a15 100644 --- a/frontend/src/components/aiops/PredictiveCapacityTab.tsx +++ b/frontend/src/components/aiops/PredictiveCapacityTab.tsx @@ -1,7 +1,7 @@ import { HardDrive, Database, Zap, Loader2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import type { CapacityForecast } from './AIPredictiveAnalytics'; +import type { CapacityForecast } from './predictUtils'; export function PredictiveCapacityTab({ forecasts, diff --git a/frontend/src/components/aiops/predictUtils.ts b/frontend/src/components/aiops/predictUtils.ts new file mode 100644 index 0000000..d2af21e --- /dev/null +++ b/frontend/src/components/aiops/predictUtils.ts @@ -0,0 +1,320 @@ +/** + * AI 预测分析模块的纯计算工具 — 类型定义、常量、线性回归、预测推算 + * + * 从 AIPredictiveAnalytics.tsx(670行) 提取,避免主组件超 700 行硬上限。 + * 所有函数均为纯函数(输入 → 输出,无副作用),便于单元测试和复用。 + * // Logan.Li + */ + +// ============ 类型定义 ============ + +export interface PredictionMetric { + name: string; + current: number; + predicted: number; + unit: string; + trend: 'up' | 'down' | 'stable'; + confidence: number; + timeRange: string; +} + +export interface CapacityForecast { + resource: string; + metricKey: string; + currentUsage: number; + predictedUsage: number; + threshold: number; + daysUntilThreshold: number; + recommendation: string; +} + +export interface AnomalyPrediction { + metric: string; + predictedAnomaly: string; + probability: number; + timeframe: string; + suggestedAction: string; +} + +// ============ 常量 ============ + +/** 指标定义:后端 metric name → 展示标签 / 告警表达式前缀 / 阈值 */ +export const METRIC_DEFS = [ + { key: 'cpu', label: 'CPU使用率', exprKey: 'cpu_usage', threshold: 85 }, + { key: 'memory', label: '内存使用率', exprKey: 'memory_usage', threshold: 85 }, + { key: 'disk', label: '磁盘使用率', exprKey: 'disk_usage', threshold: 90 }, +] as const; + +export const TIME_RANGE_LABELS: Record = { + '1h': '1小时后', + '24h': '24小时后', + '7d': '7天后', + '30d': '30天后', +}; + +export const TIME_RANGE_HOURS: Record = { + '1h': 1, + '24h': 24, + '7d': 168, + '30d': 720, +}; + +// ============ 纯函数工具 ============ + +/** 简单最小二乘线性回归,返回斜率和 R² */ +export function linearRegression(values: number[]): { slope: number; r2: number } { + const n = values.length; + if (n < 2) return { slope: 0, r2: 0 }; + const xs = values.map((_, i) => i); + const meanX = xs.reduce((a, b) => a + b, 0) / n; + const meanY = values.reduce((a, b) => a + b, 0) / n; + let num = 0, + den = 0, + ssTot = 0, + ssRes = 0; + for (let i = 0; i < n; i++) { + num += (xs[i] - meanX) * (values[i] - meanY); + den += (xs[i] - meanX) ** 2; + ssTot += (values[i] - meanY) ** 2; + } + const slope = den === 0 ? 0 : num / den; + const intercept = meanY - slope * meanX; + for (let i = 0; i < n; i++) { + const pred = intercept + slope * xs[i]; + ssRes += (values[i] - pred) ** 2; + } + const r2 = ssTot === 0 ? 1 : Math.max(0, 1 - ssRes / ssTot); + return { slope, r2 }; +} + +/** 从时序指标中提取按时间排序的数值序列 */ +export function extractSeries(metrics: any[], name: string): number[] { + return metrics + .filter((m) => m.name === name) + .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) + .map((m) => Number(m.value) || 0); +} + +/** 从告警表达式中解析阈值,如 "cpu_usage > 80" → 80 */ +export function parseThreshold(expr: string): number | null { + const m = expr.match(/[><=]\s*([\d.]+)/); + return m ? parseFloat(m[1]) : null; +} + +/** 容量规划建议 */ +export function capacityRecommendation(metricKey: string, days: number): string { + const level = days <= 7 ? 'urgent' : days <= 14 ? 'warning' : 'normal'; + const recs: Record> = { + urgent: { + cpu: 'CPU增长趋势明显,建议尽快扩容计算资源或优化高负载进程', + memory: '内存增长趋势明显,建议增加内存或排查内存泄漏', + disk: '磁盘空间即将耗尽,建议立即扩容或清理历史数据与日志', + }, + warning: { + cpu: 'CPU使用率呈上升趋势,建议持续监控并准备扩容计划', + memory: '内存使用率呈上升趋势,建议优化应用内存使用', + disk: '磁盘使用率持续增长,建议规划数据归档和清理策略', + }, + normal: { + cpu: 'CPU使用正常,建议持续监控', + memory: '内存使用正常,建议持续监控', + disk: '磁盘空间充足,建议持续监控', + }, + }; + return recs[level]?.[metricKey] || '建议持续监控资源使用趋势'; +} + +/** 异常建议操作 */ +export function anomalyAction(metricKey: string): string { + const actions: Record = { + cpu: '检查高负载进程,优化CPU密集型任务,考虑横向扩容', + memory: '排查内存泄漏,优化进程内存配置,考虑增加节点', + disk: '清理历史数据和日志,实施数据归档策略,扩容磁盘', + service: '检查服务健康状态与依赖组件,查看服务日志', + pod: '检查Pod重启原因,查看容器日志,排查OOM或健康检查失败', + }; + return actions[metricKey] || '建议检查相关服务状态并持续监控'; +} + +// ============ 综合预测推算 ============ + +/** computePredictions 的输入参数 */ +export interface ComputePredictionsInput { + rawMetrics: any[]; + serverStats: any; + alertEvents: any[]; + alertRules: any[]; + selectedTimeRange: string; // '1h' | '24h' | '7d' | '30d' +} + +/** computePredictions 的聚合输出 */ +export interface ComputedPredictions { + predictions: PredictionMetric[]; + capacityForecasts: CapacityForecast[]; + dedupedAnomalies: AnomalyPrediction[]; + accuracy: number; + predictedAlertCount: number; + capacityWarningCount: number; +} + +/** + * 综合预测推算:基于时序指标 + 告警规则,计算指标预测、容量规划、异常预测。 + * 主组件 AIPredictiveAnalytics.tsx 直接调用此函数获取所有展示数据。 + * // Logan.Li + */ +export function computePredictions(input: ComputePredictionsInput): ComputedPredictions { + const { rawMetrics, serverStats, alertEvents, alertRules, selectedTimeRange } = input; + const horizonHours = TIME_RANGE_HOURS[selectedTimeRange] || 24; + const timeRangeLabel = TIME_RANGE_LABELS[selectedTimeRange] || '24小时后'; + + // ---- 指标预测 ---- + const predictions: PredictionMetric[] = METRIC_DEFS.map((def) => { + const series = extractSeries(rawMetrics, def.key); + const { slope, r2 } = linearRegression(series); + const avgFallback = + def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk; + const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0; + let predicted = current + slope * horizonHours; + predicted = Math.max(0, Math.min(100, predicted)); + const trend: 'up' | 'down' | 'stable' = slope > 0.05 ? 'up' : slope < -0.05 ? 'down' : 'stable'; + const confidence = Math.round(60 + r2 * 35); + return { + name: def.label, + current: Math.round(current), + predicted: Math.round(predicted), + unit: '%', + trend, + confidence, + timeRange: timeRangeLabel, + }; + }); + + // ---- 容量规划 ---- + const capacityForecasts: CapacityForecast[] = METRIC_DEFS.map((def) => { + const series = extractSeries(rawMetrics, def.key); + const { slope } = linearRegression(series); + const avgFallback = + def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk; + const currentUsage = Math.round(series.length > 0 ? series[series.length - 1] : avgFallback || 0); + const slopePerDay = slope * 24; + const predictedUsage = Math.round(Math.min(100, currentUsage + slopePerDay * 7)); + const threshold = def.threshold; + let daysUntilThreshold = 999; + if (slopePerDay > 0.01) { + daysUntilThreshold = Math.round((threshold - currentUsage) / slopePerDay); + daysUntilThreshold = daysUntilThreshold < 1 ? 1 : daysUntilThreshold; + } + const resourceNames: Record = { + cpu: '服务器CPU算力', + memory: '服务器内存', + disk: '存储磁盘空间', + }; + return { + resource: resourceNames[def.key] || def.label, + metricKey: def.key, + currentUsage, + predictedUsage, + threshold, + daysUntilThreshold, + recommendation: capacityRecommendation(def.key, daysUntilThreshold), + }; + }).filter((f) => f.currentUsage > 0); + + // ---- 异常预测(基于告警规则 + 趋势 + 事件频率) ---- + const anomalyPredictions: AnomalyPrediction[] = []; + + for (const rule of alertRules) { + if (!rule.enabled) continue; + const threshold = parseThreshold(rule.expression || ''); + const exprLower = (rule.expression || '').toLowerCase(); + const matchedDef = METRIC_DEFS.find((def) => exprLower.includes(def.exprKey)); + + if (matchedDef && threshold) { + const series = extractSeries(rawMetrics, matchedDef.key); + const { slope } = linearRegression(series); + const avgFallback = + matchedDef.key === 'cpu' + ? serverStats.avg_cpu + : matchedDef.key === 'memory' + ? serverStats.avg_memory + : serverStats.avg_disk; + const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0; + const ratio = threshold > 0 ? current / threshold : 0; + let probability = Math.round(ratio * 55); + if (slope > 0.05) probability += 15; + const eventCount = alertEvents.filter( + (a) => a.rule_id === rule.id || a.rule_name === rule.name, + ).length; + probability += Math.min(20, eventCount * 3); + probability = Math.max(5, Math.min(90, probability)); + + // 时间范围:基于斜率估算何时触及阈值 + let timeframe = '未来24小时'; + if (slope > 0.01 && threshold > current) { + const hours = (threshold - current) / slope; + if (hours < 4) timeframe = '未来4小时'; + else if (hours < 12) timeframe = '未来12小时'; + else if (hours < 24) timeframe = '未来24小时'; + else timeframe = `未来${Math.ceil(hours / 24)}天`; + } + + anomalyPredictions.push({ + metric: rule.name, + predictedAnomaly: rule.description || `${rule.name}可能触发`, + probability, + timeframe, + suggestedAction: anomalyAction(matchedDef.key), + }); + } else { + // 非 metric 规则(如服务不可用、Pod重启)基于事件频率 + const eventCount = alertEvents.filter( + (a) => a.rule_id === rule.id || a.rule_name === rule.name, + ).length; + if (eventCount > 0) { + const exprKey = exprLower.includes('service') ? 'service' : exprLower.includes('pod') ? 'pod' : 'service'; + anomalyPredictions.push({ + metric: rule.name, + predictedAnomaly: rule.description || `${rule.name}可能再次发生`, + probability: Math.min(60, 20 + eventCount * 5), + timeframe: '未来24小时', + suggestedAction: anomalyAction(exprKey), + }); + } + } + } + + // 去重:同一指标名只保留概率最高的一条 + const dedupedAnomalies = anomalyPredictions + .sort((a, b) => b.probability - a.probability) + .filter((item, idx, arr) => arr.findIndex((x) => x.metric === item.metric) === idx); + + // ---- 概览统计 ---- + const avgR2 = METRIC_DEFS.length > 0 + ? METRIC_DEFS.reduce((sum, def) => sum + linearRegression(extractSeries(rawMetrics, def.key)).r2, 0) / + METRIC_DEFS.length + : 0; + const accuracy = Math.round(78 + avgR2 * 17); + const predictedAlertCount = dedupedAnomalies.filter((a) => a.probability >= 50).length; + const capacityWarningCount = capacityForecasts.filter((f) => f.daysUntilThreshold <= 14).length; + + return { + predictions, + capacityForecasts, + dedupedAnomalies, + accuracy, + predictedAlertCount, + capacityWarningCount, + }; +} + +/** 趋势图标颜色工具,供主组件 render 用 */ +export function trendColorClass(trend: string): string { + switch (trend) { + case 'up': + return 'text-red-400'; + case 'down': + return 'text-emerald-400'; + default: + return 'text-slate-400'; + } +}